clutch-cli 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clutch_cli-0.2.0/PKG-INFO +112 -0
- clutch_cli-0.2.0/README.md +89 -0
- clutch_cli-0.2.0/clutch_cli/__init__.py +3 -0
- clutch_cli-0.2.0/clutch_cli/api.py +16 -0
- clutch_cli-0.2.0/clutch_cli/auth.py +132 -0
- clutch_cli-0.2.0/clutch_cli/config.py +36 -0
- clutch_cli-0.2.0/clutch_cli/insight.py +63 -0
- clutch_cli-0.2.0/clutch_cli/main.py +43 -0
- clutch_cli-0.2.0/clutch_cli/patterns.py +92 -0
- clutch_cli-0.2.0/clutch_cli/repos.py +67 -0
- clutch_cli-0.2.0/clutch_cli/stats.py +55 -0
- clutch_cli-0.2.0/clutch_cli/status.py +54 -0
- clutch_cli-0.2.0/clutch_cli/streak.py +80 -0
- clutch_cli-0.2.0/clutch_cli.egg-info/PKG-INFO +112 -0
- clutch_cli-0.2.0/clutch_cli.egg-info/SOURCES.txt +19 -0
- clutch_cli-0.2.0/clutch_cli.egg-info/dependency_links.txt +1 -0
- clutch_cli-0.2.0/clutch_cli.egg-info/entry_points.txt +2 -0
- clutch_cli-0.2.0/clutch_cli.egg-info/requires.txt +3 -0
- clutch_cli-0.2.0/clutch_cli.egg-info/top_level.txt +1 -0
- clutch_cli-0.2.0/pyproject.toml +39 -0
- clutch_cli-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clutch-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: GitHub tracks your work. Clutch tracks you.
|
|
5
|
+
Author-email: Lay Patel <lay.patel.1313@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/laypatel13/clutch
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/laypatel13/clutch/issues
|
|
9
|
+
Keywords: github,developer,activity,cli,dashboard,streak
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: typer>=0.12.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: rich>=13.0.0
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
26
|
+
# ⚡ clutch-cli
|
|
27
|
+
|
|
28
|
+
### *The CLI companion for Clutch — GitHub tracks your work. Clutch tracks you.*
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/clutch-cli)
|
|
31
|
+
[](https://python.org)
|
|
32
|
+
[](https://github.com/laypatel13/clutch/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
`clutch-cli` is the terminal companion for [Clutch](https://clutch-laypatel.netlify.app) — an open-source AI-powered developer activity dashboard. Get your GitHub streaks, stats, coding patterns, and AI insights without leaving your terminal.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install clutch-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Login via GitHub (opens browser automatically, no copy-paste needed)
|
|
50
|
+
clutch auth login
|
|
51
|
+
|
|
52
|
+
# Check your streak
|
|
53
|
+
clutch streak
|
|
54
|
+
|
|
55
|
+
# View your stats
|
|
56
|
+
clutch stats
|
|
57
|
+
|
|
58
|
+
# Get your AI weekly insight
|
|
59
|
+
clutch insight
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `clutch auth login` | Login via GitHub OAuth (fully automatic) |
|
|
67
|
+
| `clutch auth logout` | Logout and clear saved credentials |
|
|
68
|
+
| `clutch auth whoami` | Show currently logged-in user |
|
|
69
|
+
| `clutch streak` | Current and longest commit streak |
|
|
70
|
+
| `clutch stats` | Activity stats — commits, PRs, issues, active days |
|
|
71
|
+
| `clutch stats --days 7` | Stats for a custom time range |
|
|
72
|
+
| `clutch repos` | Your most recently active repositories |
|
|
73
|
+
| `clutch insight` | AI-generated weekly insight (powered by Groq Llama) |
|
|
74
|
+
| `clutch patterns` | Coding patterns — best day, consistency score, day distribution |
|
|
75
|
+
| `clutch status` | Login status and API health check |
|
|
76
|
+
| `clutch --version` | Show installed version |
|
|
77
|
+
|
|
78
|
+
## How Login Works
|
|
79
|
+
|
|
80
|
+
`clutch auth login` spins up a temporary local server on port `9876`, opens GitHub OAuth in your browser, and automatically captures the token when GitHub redirects back. No copy-pasting required.
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
$ clutch auth login
|
|
84
|
+
|
|
85
|
+
⚡ Clutch Login
|
|
86
|
+
Opening GitHub in your browser...
|
|
87
|
+
Waiting for GitHub authorization...
|
|
88
|
+
|
|
89
|
+
✅ Logged in as @laypatel13
|
|
90
|
+
Welcome to Clutch, Lay Patel!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
After the first login, your token is saved in `~/.clutch/config.json` and all commands work silently.
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
By default the CLI talks to the hosted Clutch API. To point it at a local backend:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
export CLUTCH_API_URL=http://localhost:8000
|
|
101
|
+
clutch auth login
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Links
|
|
105
|
+
|
|
106
|
+
- 🌐 [Live Dashboard](https://clutch-laypatel.netlify.app)
|
|
107
|
+
- 📖 [Full Documentation](https://github.com/laypatel13/clutch)
|
|
108
|
+
- 🐛 [Report a Bug](https://github.com/laypatel13/clutch/issues)
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT © [Lay Patel](https://github.com/laypatel13)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ⚡ clutch-cli
|
|
4
|
+
|
|
5
|
+
### *The CLI companion for Clutch — GitHub tracks your work. Clutch tracks you.*
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/clutch-cli)
|
|
8
|
+
[](https://python.org)
|
|
9
|
+
[](https://github.com/laypatel13/clutch/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
`clutch-cli` is the terminal companion for [Clutch](https://clutch-laypatel.netlify.app) — an open-source AI-powered developer activity dashboard. Get your GitHub streaks, stats, coding patterns, and AI insights without leaving your terminal.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install clutch-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Login via GitHub (opens browser automatically, no copy-paste needed)
|
|
27
|
+
clutch auth login
|
|
28
|
+
|
|
29
|
+
# Check your streak
|
|
30
|
+
clutch streak
|
|
31
|
+
|
|
32
|
+
# View your stats
|
|
33
|
+
clutch stats
|
|
34
|
+
|
|
35
|
+
# Get your AI weekly insight
|
|
36
|
+
clutch insight
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `clutch auth login` | Login via GitHub OAuth (fully automatic) |
|
|
44
|
+
| `clutch auth logout` | Logout and clear saved credentials |
|
|
45
|
+
| `clutch auth whoami` | Show currently logged-in user |
|
|
46
|
+
| `clutch streak` | Current and longest commit streak |
|
|
47
|
+
| `clutch stats` | Activity stats — commits, PRs, issues, active days |
|
|
48
|
+
| `clutch stats --days 7` | Stats for a custom time range |
|
|
49
|
+
| `clutch repos` | Your most recently active repositories |
|
|
50
|
+
| `clutch insight` | AI-generated weekly insight (powered by Groq Llama) |
|
|
51
|
+
| `clutch patterns` | Coding patterns — best day, consistency score, day distribution |
|
|
52
|
+
| `clutch status` | Login status and API health check |
|
|
53
|
+
| `clutch --version` | Show installed version |
|
|
54
|
+
|
|
55
|
+
## How Login Works
|
|
56
|
+
|
|
57
|
+
`clutch auth login` spins up a temporary local server on port `9876`, opens GitHub OAuth in your browser, and automatically captures the token when GitHub redirects back. No copy-pasting required.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
$ clutch auth login
|
|
61
|
+
|
|
62
|
+
⚡ Clutch Login
|
|
63
|
+
Opening GitHub in your browser...
|
|
64
|
+
Waiting for GitHub authorization...
|
|
65
|
+
|
|
66
|
+
✅ Logged in as @laypatel13
|
|
67
|
+
Welcome to Clutch, Lay Patel!
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
After the first login, your token is saved in `~/.clutch/config.json` and all commands work silently.
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
By default the CLI talks to the hosted Clutch API. To point it at a local backend:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export CLUTCH_API_URL=http://localhost:8000
|
|
78
|
+
clutch auth login
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Links
|
|
82
|
+
|
|
83
|
+
- 🌐 [Live Dashboard](https://clutch-laypatel.netlify.app)
|
|
84
|
+
- 📖 [Full Documentation](https://github.com/laypatel13/clutch)
|
|
85
|
+
- 🐛 [Report a Bug](https://github.com/laypatel13/clutch/issues)
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT © [Lay Patel](https://github.com/laypatel13)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import typer
|
|
3
|
+
from clutch_cli.config import get_token, API_BASE_URL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_client() -> httpx.Client:
|
|
7
|
+
"""Get authenticated HTTP client."""
|
|
8
|
+
token = get_token()
|
|
9
|
+
if not token:
|
|
10
|
+
typer.echo("❌ You are not logged in. Run: clutch auth login")
|
|
11
|
+
raise typer.Exit(1)
|
|
12
|
+
return httpx.Client(
|
|
13
|
+
base_url=API_BASE_URL,
|
|
14
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
15
|
+
timeout=15,
|
|
16
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
3
|
+
from urllib.parse import parse_qs, urlparse
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from clutch_cli.config import API_BASE_URL, clear_config, get_username, save_token
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Authentication commands.")
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
CLI_CALLBACK_PORT = 9876
|
|
15
|
+
_captured_token: dict = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
19
|
+
"""Minimal HTTP handler that captures the JWT from the OAuth redirect."""
|
|
20
|
+
|
|
21
|
+
def do_GET(self):
|
|
22
|
+
parsed = urlparse(self.path)
|
|
23
|
+
if parsed.path == "/callback":
|
|
24
|
+
params = parse_qs(parsed.query)
|
|
25
|
+
token = params.get("token", [None])[0]
|
|
26
|
+
if token:
|
|
27
|
+
_captured_token["value"] = token
|
|
28
|
+
self._respond(200, _success_page())
|
|
29
|
+
else:
|
|
30
|
+
self._respond(400, _error_page("No token received."))
|
|
31
|
+
else:
|
|
32
|
+
self._respond(404, b"Not found")
|
|
33
|
+
|
|
34
|
+
def _respond(self, status: int, body: bytes) -> None:
|
|
35
|
+
self.send_response(status)
|
|
36
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
37
|
+
self.end_headers()
|
|
38
|
+
self.wfile.write(body)
|
|
39
|
+
|
|
40
|
+
def log_message(self, *args):
|
|
41
|
+
pass # silence default request logging
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _success_page() -> bytes:
|
|
45
|
+
return b"""<!DOCTYPE html>
|
|
46
|
+
<html>
|
|
47
|
+
<head><title>Clutch Login</title></head>
|
|
48
|
+
<body style="font-family:sans-serif;text-align:center;padding:60px;background:#0d1117;color:#58a6ff">
|
|
49
|
+
<h1>⚡ Clutch</h1>
|
|
50
|
+
<p style="color:#3fb950;font-size:1.2rem">✓ Login successful! You can close this tab.</p>
|
|
51
|
+
</body>
|
|
52
|
+
</html>"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _error_page(msg: str) -> bytes:
|
|
56
|
+
return f"""<!DOCTYPE html>
|
|
57
|
+
<html>
|
|
58
|
+
<head><title>Clutch Login Error</title></head>
|
|
59
|
+
<body style="font-family:sans-serif;text-align:center;padding:60px;background:#0d1117;color:#f85149">
|
|
60
|
+
<h1>⚡ Clutch</h1>
|
|
61
|
+
<p>Login failed: {msg}</p>
|
|
62
|
+
</body>
|
|
63
|
+
</html>""".encode()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def login():
|
|
68
|
+
"""Login to Clutch via GitHub OAuth (browser-based, fully automatic)."""
|
|
69
|
+
console.print("\n[bold green]⚡ Clutch Login[/bold green]")
|
|
70
|
+
console.print("[dim]Starting local callback listener...[/dim]")
|
|
71
|
+
|
|
72
|
+
_captured_token.clear()
|
|
73
|
+
server = HTTPServer(("localhost", CLI_CALLBACK_PORT), _CallbackHandler)
|
|
74
|
+
|
|
75
|
+
login_url = f"{API_BASE_URL}/auth/github?cli=true"
|
|
76
|
+
console.print("[dim]Opening GitHub in your browser...[/dim]\n")
|
|
77
|
+
webbrowser.open(login_url)
|
|
78
|
+
|
|
79
|
+
console.print("[yellow]Waiting for GitHub authorization...[/yellow]")
|
|
80
|
+
console.print("[dim](If your browser didn't open, visit:)[/dim]")
|
|
81
|
+
console.print(f"[dim]{login_url}[/dim]\n")
|
|
82
|
+
|
|
83
|
+
# Block on main thread until the OAuth callback hits localhost:9876/callback
|
|
84
|
+
server.handle_request()
|
|
85
|
+
server.server_close()
|
|
86
|
+
|
|
87
|
+
token = _captured_token.get("value")
|
|
88
|
+
if not token:
|
|
89
|
+
console.print("[red]❌ Login failed — no token received.[/red]")
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
# Verify token and fetch user info
|
|
93
|
+
try:
|
|
94
|
+
response = httpx.get(
|
|
95
|
+
f"{API_BASE_URL}/users/me",
|
|
96
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
97
|
+
timeout=10,
|
|
98
|
+
)
|
|
99
|
+
if response.status_code != 200:
|
|
100
|
+
console.print("[red]❌ Token validation failed.[/red]")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
user = response.json()
|
|
104
|
+
save_token(token, user["username"])
|
|
105
|
+
console.print(f"[bold green]✅ Logged in as @{user['username']}[/bold green]")
|
|
106
|
+
console.print(f"[dim]Welcome to Clutch, {user.get('name') or user['username']}![/dim]\n")
|
|
107
|
+
|
|
108
|
+
except httpx.RequestError:
|
|
109
|
+
console.print("[red]❌ Could not connect to Clutch API.[/red]")
|
|
110
|
+
raise typer.Exit(1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def logout():
|
|
115
|
+
"""Logout from Clutch."""
|
|
116
|
+
username = get_username()
|
|
117
|
+
if not username:
|
|
118
|
+
console.print("[yellow]You are not logged in.[/yellow]")
|
|
119
|
+
raise typer.Exit()
|
|
120
|
+
|
|
121
|
+
clear_config()
|
|
122
|
+
console.print("[bold green]✅ Logged out successfully.[/bold green]")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command()
|
|
126
|
+
def whoami():
|
|
127
|
+
"""Show the currently logged-in user."""
|
|
128
|
+
username = get_username()
|
|
129
|
+
if not username:
|
|
130
|
+
console.print("[yellow]Not logged in. Run: clutch auth login[/yellow]")
|
|
131
|
+
raise typer.Exit()
|
|
132
|
+
console.print(f"[bold green]Logged in as @{username}[/bold green]")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
CONFIG_DIR = Path.home() / ".clutch"
|
|
6
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
7
|
+
API_BASE_URL = os.getenv("CLUTCH_API_URL", "https://clutch-api-7lw4.onrender.com")
|
|
8
|
+
|
|
9
|
+
def save_token(token: str, username: str) -> None:
|
|
10
|
+
"""Save auth token to local config file."""
|
|
11
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
12
|
+
config = {"token": token, "username": username}
|
|
13
|
+
CONFIG_FILE.write_text(json.dumps(config, indent=2))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_config() -> dict:
|
|
17
|
+
"""Load config from local file."""
|
|
18
|
+
if not CONFIG_FILE.exists():
|
|
19
|
+
return {}
|
|
20
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_token() -> str | None:
|
|
24
|
+
"""Get saved auth token."""
|
|
25
|
+
return load_config().get("token")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_username() -> str | None:
|
|
29
|
+
"""Get saved username."""
|
|
30
|
+
return load_config().get("username")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def clear_config() -> None:
|
|
34
|
+
"""Remove saved config."""
|
|
35
|
+
if CONFIG_FILE.exists():
|
|
36
|
+
CONFIG_FILE.unlink()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
from rich import box
|
|
6
|
+
from clutch_cli.api import get_client
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def insight():
|
|
12
|
+
"""Get your AI-generated weekly insight."""
|
|
13
|
+
with get_client() as client:
|
|
14
|
+
try:
|
|
15
|
+
console.print()
|
|
16
|
+
console.print("[dim]Generating insight...[/dim]")
|
|
17
|
+
|
|
18
|
+
response = client.get("/insights/weekly")
|
|
19
|
+
if response.status_code != 200:
|
|
20
|
+
console.print("[red]Error: Failed to fetch insight.[/red]")
|
|
21
|
+
raise typer.Exit(1)
|
|
22
|
+
|
|
23
|
+
data = response.json()
|
|
24
|
+
|
|
25
|
+
if "message" in data:
|
|
26
|
+
console.print(f"[yellow]{data['message']}[/yellow]")
|
|
27
|
+
raise typer.Exit()
|
|
28
|
+
|
|
29
|
+
stats = data["stats"]
|
|
30
|
+
summary = data["ai_summary"]
|
|
31
|
+
|
|
32
|
+
console.print()
|
|
33
|
+
console.rule("[bold blue]⚡ CLUTCH — WEEKLY INSIGHT[/bold blue]")
|
|
34
|
+
console.print()
|
|
35
|
+
|
|
36
|
+
# AI summary block
|
|
37
|
+
console.print(f"[dim]Week of[/dim] [white]{data['week_start']}[/white]")
|
|
38
|
+
console.print()
|
|
39
|
+
console.print(Text(summary, style="white"), soft_wrap=True)
|
|
40
|
+
console.print()
|
|
41
|
+
console.rule(style="dim")
|
|
42
|
+
console.print()
|
|
43
|
+
|
|
44
|
+
# Stats row
|
|
45
|
+
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
46
|
+
table.add_column("Label", style="dim", width=22)
|
|
47
|
+
table.add_column("Value", style="bold white")
|
|
48
|
+
|
|
49
|
+
table.add_row("Commits", f"[green]{stats['total_commits']}[/green]")
|
|
50
|
+
table.add_row("Active Days", f"[green]{stats['active_days']}[/green]")
|
|
51
|
+
table.add_row("Pull Requests", str(stats["total_prs"]))
|
|
52
|
+
table.add_row("Issues", str(stats["total_issues"]))
|
|
53
|
+
table.add_row("Best Day", str(stats["best_day"]))
|
|
54
|
+
table.add_row("Generated by", f"[dim]{data['generated_by']}[/dim]")
|
|
55
|
+
|
|
56
|
+
console.print(table)
|
|
57
|
+
console.print()
|
|
58
|
+
console.rule(style="dim")
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
except Exception:
|
|
62
|
+
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
63
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from clutch_cli import auth, streak, stats, insight, repos, patterns, status
|
|
3
|
+
|
|
4
|
+
__version__ = "0.2.0"
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(
|
|
7
|
+
name="clutch",
|
|
8
|
+
help="GitHub tracks your work. Clutch tracks you.",
|
|
9
|
+
no_args_is_help=True,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _version_callback(value: bool):
|
|
14
|
+
if value:
|
|
15
|
+
typer.echo(f"clutch v{__version__}")
|
|
16
|
+
raise typer.Exit()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback()
|
|
20
|
+
def main(
|
|
21
|
+
version: bool = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
"--version",
|
|
24
|
+
"-v",
|
|
25
|
+
help="Show version and exit.",
|
|
26
|
+
callback=_version_callback,
|
|
27
|
+
is_eager=True,
|
|
28
|
+
),
|
|
29
|
+
):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
app.add_typer(auth.app, name="auth")
|
|
34
|
+
app.command()(streak.streak)
|
|
35
|
+
app.command()(stats.stats)
|
|
36
|
+
app.command()(insight.insight)
|
|
37
|
+
app.command()(repos.repos)
|
|
38
|
+
app.command()(patterns.patterns)
|
|
39
|
+
app.command()(status.status)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
app()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich import box
|
|
5
|
+
from clutch_cli.api import get_client
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def patterns():
|
|
11
|
+
"""Show your coding patterns and habits."""
|
|
12
|
+
with get_client() as client:
|
|
13
|
+
try:
|
|
14
|
+
console.print()
|
|
15
|
+
console.print("[dim]Analyzing patterns...[/dim]")
|
|
16
|
+
|
|
17
|
+
response = client.get("/insights/patterns")
|
|
18
|
+
if response.status_code != 200:
|
|
19
|
+
console.print("[red]Error: Failed to fetch patterns.[/red]")
|
|
20
|
+
raise typer.Exit(1)
|
|
21
|
+
|
|
22
|
+
data = response.json()
|
|
23
|
+
|
|
24
|
+
if "message" in data:
|
|
25
|
+
console.print(f"[yellow]{data['message']}[/yellow]")
|
|
26
|
+
raise typer.Exit()
|
|
27
|
+
|
|
28
|
+
console.print()
|
|
29
|
+
console.rule("[bold blue]⚡ CLUTCH — CODING PATTERNS[/bold blue]")
|
|
30
|
+
console.print()
|
|
31
|
+
|
|
32
|
+
# Summary table
|
|
33
|
+
score = data["consistency_score"]
|
|
34
|
+
score_color = "green" if score >= 70 else "yellow" if score >= 40 else "red"
|
|
35
|
+
|
|
36
|
+
summary = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
37
|
+
summary.add_column("Label", style="dim", width=22)
|
|
38
|
+
summary.add_column("Value", style="bold white")
|
|
39
|
+
|
|
40
|
+
summary.add_row("Best Day", f"[green]{data['best_day']}[/green]")
|
|
41
|
+
summary.add_row("Worst Day", f"[dim]{data['worst_day']}[/dim]")
|
|
42
|
+
summary.add_row("Avg Daily Commits", str(data["avg_daily_commits"]))
|
|
43
|
+
summary.add_row("Total Active Days", str(data["total_active_days"]))
|
|
44
|
+
summary.add_row(
|
|
45
|
+
"Consistency Score",
|
|
46
|
+
f"[{score_color}]{score}%[/{score_color}]",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
console.print(summary)
|
|
50
|
+
console.print()
|
|
51
|
+
console.rule("[dim]Activity by Day[/dim]", style="dim")
|
|
52
|
+
console.print()
|
|
53
|
+
|
|
54
|
+
# Day distribution bar chart
|
|
55
|
+
day_table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
56
|
+
day_table.add_column("Day", style="dim", width=12)
|
|
57
|
+
day_table.add_column("Bar", width=32)
|
|
58
|
+
day_table.add_column("Count", justify="right", style="bold white", width=6)
|
|
59
|
+
|
|
60
|
+
max_commits = max(data["day_distribution"].values()) or 1
|
|
61
|
+
for day, commits in data["day_distribution"].items():
|
|
62
|
+
filled = int((commits / max_commits) * 28)
|
|
63
|
+
is_best = day == data["best_day"]
|
|
64
|
+
bar_color = "blue" if is_best else "white"
|
|
65
|
+
bar = f"[{bar_color}]{'█' * filled}[/{bar_color}][dim]{'░' * (28 - filled)}[/dim]"
|
|
66
|
+
day_label = f"[blue]{day}[/blue]" if is_best else day
|
|
67
|
+
day_table.add_row(day_label, bar, str(commits))
|
|
68
|
+
|
|
69
|
+
console.print(day_table)
|
|
70
|
+
|
|
71
|
+
# Top repos
|
|
72
|
+
if data.get("top_repos"):
|
|
73
|
+
console.print()
|
|
74
|
+
console.rule("[dim]Most Active Repos[/dim]", style="dim")
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
repo_table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
78
|
+
repo_table.add_column("Repo", style="bold white", width=30)
|
|
79
|
+
repo_table.add_column("Active Days", justify="right", style="dim", width=12)
|
|
80
|
+
|
|
81
|
+
for repo in data["top_repos"]:
|
|
82
|
+
repo_table.add_row(repo["repo"], str(repo["days_active"]))
|
|
83
|
+
|
|
84
|
+
console.print(repo_table)
|
|
85
|
+
|
|
86
|
+
console.print()
|
|
87
|
+
console.rule(style="dim")
|
|
88
|
+
console.print()
|
|
89
|
+
|
|
90
|
+
except Exception:
|
|
91
|
+
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
92
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich import box
|
|
5
|
+
from clutch_cli.api import get_client
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
LANG_COLORS = {
|
|
10
|
+
"Python": "blue",
|
|
11
|
+
"TypeScript": "cyan",
|
|
12
|
+
"JavaScript": "yellow",
|
|
13
|
+
"Go": "cyan",
|
|
14
|
+
"Rust": "red",
|
|
15
|
+
"Java": "red",
|
|
16
|
+
"C++": "blue",
|
|
17
|
+
"C": "blue",
|
|
18
|
+
"Shell": "green",
|
|
19
|
+
"HTML": "red",
|
|
20
|
+
"CSS": "blue",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def repos():
|
|
25
|
+
"""Show your most recently active repositories."""
|
|
26
|
+
with get_client() as client:
|
|
27
|
+
try:
|
|
28
|
+
response = client.get("/github/repos")
|
|
29
|
+
if response.status_code != 200:
|
|
30
|
+
console.print("[red]Error: Failed to fetch repositories.[/red]")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
|
|
33
|
+
repos_data = response.json()
|
|
34
|
+
|
|
35
|
+
console.print()
|
|
36
|
+
console.rule("[bold blue]⚡ CLUTCH — REPOSITORIES[/bold blue]")
|
|
37
|
+
console.print()
|
|
38
|
+
|
|
39
|
+
table = Table(box=box.SIMPLE, show_header=True, pad_edge=False)
|
|
40
|
+
table.add_column("Repository", style="bold white", width=28)
|
|
41
|
+
table.add_column("Language", width=14)
|
|
42
|
+
table.add_column("Stars", justify="right", width=7)
|
|
43
|
+
table.add_column("Forks", justify="right", width=7)
|
|
44
|
+
table.add_column("Updated", style="dim", width=12)
|
|
45
|
+
|
|
46
|
+
for repo in repos_data[:10]:
|
|
47
|
+
lang = repo.get("language") or "—"
|
|
48
|
+
lang_color = LANG_COLORS.get(lang, "white")
|
|
49
|
+
stars = repo.get("stargazers_count", 0)
|
|
50
|
+
forks = repo.get("forks_count", 0)
|
|
51
|
+
|
|
52
|
+
table.add_row(
|
|
53
|
+
repo["name"],
|
|
54
|
+
f"[{lang_color}]{lang}[/{lang_color}]",
|
|
55
|
+
f"[yellow]{stars}[/yellow]" if stars > 0 else "[dim]0[/dim]",
|
|
56
|
+
str(forks) if forks > 0 else "[dim]0[/dim]",
|
|
57
|
+
repo["updated_at"][:10],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
console.print(table)
|
|
61
|
+
console.print()
|
|
62
|
+
console.rule(style="dim")
|
|
63
|
+
console.print()
|
|
64
|
+
|
|
65
|
+
except Exception:
|
|
66
|
+
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
67
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich import box
|
|
5
|
+
from clutch_cli.api import get_client
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stats(days: int = typer.Option(30, help="Number of days to look back.")):
|
|
11
|
+
"""Show your GitHub activity stats."""
|
|
12
|
+
with get_client() as client:
|
|
13
|
+
try:
|
|
14
|
+
response = client.get(f"/github/activity?days={days}")
|
|
15
|
+
if response.status_code != 200:
|
|
16
|
+
console.print("[red]Error: Failed to fetch activity data.[/red]")
|
|
17
|
+
raise typer.Exit(1)
|
|
18
|
+
|
|
19
|
+
data = response.json()
|
|
20
|
+
commits = data["total_commits"]
|
|
21
|
+
prs = data["total_prs"]
|
|
22
|
+
issues = data["total_issues"]
|
|
23
|
+
active = data["active_days"]
|
|
24
|
+
|
|
25
|
+
console.print()
|
|
26
|
+
console.rule(f"[bold blue]⚡ CLUTCH — STATS · LAST {days} DAYS[/bold blue]")
|
|
27
|
+
console.print()
|
|
28
|
+
|
|
29
|
+
table = Table(box=box.SIMPLE, show_header=True, pad_edge=False)
|
|
30
|
+
table.add_column("Metric", style="dim", width=22)
|
|
31
|
+
table.add_column("Count", justify="right", style="bold white", width=10)
|
|
32
|
+
table.add_column("Bar", width=32)
|
|
33
|
+
|
|
34
|
+
max_val = max(commits, prs, issues, active, 1)
|
|
35
|
+
|
|
36
|
+
def bar(val):
|
|
37
|
+
filled = int((val / max_val) * 28)
|
|
38
|
+
return f"[blue]{'█' * filled}[/blue][dim]{'░' * (28 - filled)}[/dim]"
|
|
39
|
+
|
|
40
|
+
def color(val):
|
|
41
|
+
return "bold green" if val > 0 else "dim"
|
|
42
|
+
|
|
43
|
+
table.add_row("Commits", f"[{color(commits)}]{commits}[/{color(commits)}]", bar(commits))
|
|
44
|
+
table.add_row("Pull Requests", f"[{color(prs)}]{prs}[/{color(prs)}]", bar(prs))
|
|
45
|
+
table.add_row("Issues", f"[{color(issues)}]{issues}[/{color(issues)}]", bar(issues))
|
|
46
|
+
table.add_row("Active Days", f"[{color(active)}]{active}[/{color(active)}]", bar(active))
|
|
47
|
+
|
|
48
|
+
console.print(table)
|
|
49
|
+
console.print()
|
|
50
|
+
console.rule(style="dim")
|
|
51
|
+
console.print()
|
|
52
|
+
|
|
53
|
+
except Exception:
|
|
54
|
+
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
55
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from rich import box
|
|
6
|
+
from clutch_cli.config import API_BASE_URL, get_token, get_username
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def status():
|
|
12
|
+
"""Show login status and API health."""
|
|
13
|
+
username = get_username()
|
|
14
|
+
token = get_token()
|
|
15
|
+
|
|
16
|
+
console.print()
|
|
17
|
+
console.rule("[bold blue]⚡ CLUTCH — STATUS[/bold blue]")
|
|
18
|
+
console.print()
|
|
19
|
+
|
|
20
|
+
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
21
|
+
table.add_column("Check", style="dim", width=18)
|
|
22
|
+
table.add_column("Result", style="bold white")
|
|
23
|
+
|
|
24
|
+
if not username or not token:
|
|
25
|
+
table.add_row("Auth", "[red]Not logged in[/red]")
|
|
26
|
+
table.add_row("Hint", "[dim]Run: clutch auth login[/dim]")
|
|
27
|
+
console.print(table)
|
|
28
|
+
console.print()
|
|
29
|
+
console.rule(style="dim")
|
|
30
|
+
console.print()
|
|
31
|
+
raise typer.Exit()
|
|
32
|
+
|
|
33
|
+
table.add_row("User", f"[blue]@{username}[/blue]")
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
response = httpx.get(
|
|
37
|
+
f"{API_BASE_URL}/users/me",
|
|
38
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
39
|
+
timeout=8,
|
|
40
|
+
)
|
|
41
|
+
if response.status_code == 200:
|
|
42
|
+
table.add_row("Token", "[green]Valid[/green]")
|
|
43
|
+
table.add_row("API", f"[green]Reachable[/green] [dim]{API_BASE_URL}[/dim]")
|
|
44
|
+
else:
|
|
45
|
+
table.add_row("Token", f"[yellow]Expired ({response.status_code})[/yellow]")
|
|
46
|
+
table.add_row("Hint", "[dim]Run: clutch auth login[/dim]")
|
|
47
|
+
except httpx.RequestError:
|
|
48
|
+
table.add_row("Token", "[green]Saved[/green]")
|
|
49
|
+
table.add_row("API", "[red]Unreachable[/red]")
|
|
50
|
+
|
|
51
|
+
console.print(table)
|
|
52
|
+
console.print()
|
|
53
|
+
console.rule(style="dim")
|
|
54
|
+
console.print()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich import box
|
|
5
|
+
from clutch_cli.api import get_client
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def streak():
|
|
11
|
+
"""Show your current and longest commit streak."""
|
|
12
|
+
with get_client() as client:
|
|
13
|
+
try:
|
|
14
|
+
response = client.get("/github/streak")
|
|
15
|
+
if response.status_code != 200:
|
|
16
|
+
console.print("[red]Error: Failed to fetch streak data.[/red]")
|
|
17
|
+
raise typer.Exit(1)
|
|
18
|
+
|
|
19
|
+
data = response.json()
|
|
20
|
+
current = data["current_streak"]
|
|
21
|
+
longest = data["longest_streak"]
|
|
22
|
+
total_active = data["total_active_days"]
|
|
23
|
+
|
|
24
|
+
if current >= 30:
|
|
25
|
+
streak_color = "bold green"
|
|
26
|
+
status = "ON FIRE"
|
|
27
|
+
elif current >= 14:
|
|
28
|
+
streak_color = "bold green"
|
|
29
|
+
status = "STRONG"
|
|
30
|
+
elif current >= 7:
|
|
31
|
+
streak_color = "bold blue"
|
|
32
|
+
status = "BUILDING"
|
|
33
|
+
elif current > 0:
|
|
34
|
+
streak_color = "bold yellow"
|
|
35
|
+
status = "ACTIVE"
|
|
36
|
+
else:
|
|
37
|
+
streak_color = "dim"
|
|
38
|
+
status = "INACTIVE"
|
|
39
|
+
|
|
40
|
+
console.print()
|
|
41
|
+
console.rule("[bold blue]⚡ CLUTCH — STREAK[/bold blue]")
|
|
42
|
+
console.print()
|
|
43
|
+
|
|
44
|
+
table = Table(box=box.SIMPLE, show_header=False, pad_edge=False)
|
|
45
|
+
table.add_column("Label", style="dim", width=22)
|
|
46
|
+
table.add_column("Value", style="bold white")
|
|
47
|
+
table.add_column("Badge", justify="right")
|
|
48
|
+
|
|
49
|
+
table.add_row(
|
|
50
|
+
"Current Streak",
|
|
51
|
+
f"[{streak_color}]{current} DAY'S[/{streak_color}]",
|
|
52
|
+
f"[blue]{status}[/blue]",
|
|
53
|
+
)
|
|
54
|
+
table.add_row(
|
|
55
|
+
"Longest Streak",
|
|
56
|
+
f"{longest} DAY'S",
|
|
57
|
+
"",
|
|
58
|
+
)
|
|
59
|
+
table.add_row(
|
|
60
|
+
"Total Active Days",
|
|
61
|
+
f"{total_active} DAY'S",
|
|
62
|
+
"",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Visual bar: current vs longest
|
|
66
|
+
if longest > 0:
|
|
67
|
+
filled = int((current / longest) * 30)
|
|
68
|
+
bar = "█" * filled + "░" * (30 - filled)
|
|
69
|
+
console.print(table)
|
|
70
|
+
console.print(f" [dim]Progress to longest[/dim] [blue]{bar}[/blue] [dim]{current}/{longest}[/dim]")
|
|
71
|
+
else:
|
|
72
|
+
console.print(table)
|
|
73
|
+
|
|
74
|
+
console.print()
|
|
75
|
+
console.rule(style="dim")
|
|
76
|
+
console.print()
|
|
77
|
+
|
|
78
|
+
except Exception:
|
|
79
|
+
console.print("[red]Error: Could not connect to Clutch API.[/red]")
|
|
80
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clutch-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: GitHub tracks your work. Clutch tracks you.
|
|
5
|
+
Author-email: Lay Patel <lay.patel.1313@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/laypatel13/clutch
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/laypatel13/clutch/issues
|
|
9
|
+
Keywords: github,developer,activity,cli,dashboard,streak
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: typer>=0.12.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: rich>=13.0.0
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
26
|
+
# ⚡ clutch-cli
|
|
27
|
+
|
|
28
|
+
### *The CLI companion for Clutch — GitHub tracks your work. Clutch tracks you.*
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/clutch-cli)
|
|
31
|
+
[](https://python.org)
|
|
32
|
+
[](https://github.com/laypatel13/clutch/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
`clutch-cli` is the terminal companion for [Clutch](https://clutch-laypatel.netlify.app) — an open-source AI-powered developer activity dashboard. Get your GitHub streaks, stats, coding patterns, and AI insights without leaving your terminal.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install clutch-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Login via GitHub (opens browser automatically, no copy-paste needed)
|
|
50
|
+
clutch auth login
|
|
51
|
+
|
|
52
|
+
# Check your streak
|
|
53
|
+
clutch streak
|
|
54
|
+
|
|
55
|
+
# View your stats
|
|
56
|
+
clutch stats
|
|
57
|
+
|
|
58
|
+
# Get your AI weekly insight
|
|
59
|
+
clutch insight
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `clutch auth login` | Login via GitHub OAuth (fully automatic) |
|
|
67
|
+
| `clutch auth logout` | Logout and clear saved credentials |
|
|
68
|
+
| `clutch auth whoami` | Show currently logged-in user |
|
|
69
|
+
| `clutch streak` | Current and longest commit streak |
|
|
70
|
+
| `clutch stats` | Activity stats — commits, PRs, issues, active days |
|
|
71
|
+
| `clutch stats --days 7` | Stats for a custom time range |
|
|
72
|
+
| `clutch repos` | Your most recently active repositories |
|
|
73
|
+
| `clutch insight` | AI-generated weekly insight (powered by Groq Llama) |
|
|
74
|
+
| `clutch patterns` | Coding patterns — best day, consistency score, day distribution |
|
|
75
|
+
| `clutch status` | Login status and API health check |
|
|
76
|
+
| `clutch --version` | Show installed version |
|
|
77
|
+
|
|
78
|
+
## How Login Works
|
|
79
|
+
|
|
80
|
+
`clutch auth login` spins up a temporary local server on port `9876`, opens GitHub OAuth in your browser, and automatically captures the token when GitHub redirects back. No copy-pasting required.
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
$ clutch auth login
|
|
84
|
+
|
|
85
|
+
⚡ Clutch Login
|
|
86
|
+
Opening GitHub in your browser...
|
|
87
|
+
Waiting for GitHub authorization...
|
|
88
|
+
|
|
89
|
+
✅ Logged in as @laypatel13
|
|
90
|
+
Welcome to Clutch, Lay Patel!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
After the first login, your token is saved in `~/.clutch/config.json` and all commands work silently.
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
By default the CLI talks to the hosted Clutch API. To point it at a local backend:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
export CLUTCH_API_URL=http://localhost:8000
|
|
101
|
+
clutch auth login
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Links
|
|
105
|
+
|
|
106
|
+
- 🌐 [Live Dashboard](https://clutch-laypatel.netlify.app)
|
|
107
|
+
- 📖 [Full Documentation](https://github.com/laypatel13/clutch)
|
|
108
|
+
- 🐛 [Report a Bug](https://github.com/laypatel13/clutch/issues)
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT © [Lay Patel](https://github.com/laypatel13)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
clutch_cli/__init__.py
|
|
4
|
+
clutch_cli/api.py
|
|
5
|
+
clutch_cli/auth.py
|
|
6
|
+
clutch_cli/config.py
|
|
7
|
+
clutch_cli/insight.py
|
|
8
|
+
clutch_cli/main.py
|
|
9
|
+
clutch_cli/patterns.py
|
|
10
|
+
clutch_cli/repos.py
|
|
11
|
+
clutch_cli/stats.py
|
|
12
|
+
clutch_cli/status.py
|
|
13
|
+
clutch_cli/streak.py
|
|
14
|
+
clutch_cli.egg-info/PKG-INFO
|
|
15
|
+
clutch_cli.egg-info/SOURCES.txt
|
|
16
|
+
clutch_cli.egg-info/dependency_links.txt
|
|
17
|
+
clutch_cli.egg-info/entry_points.txt
|
|
18
|
+
clutch_cli.egg-info/requires.txt
|
|
19
|
+
clutch_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clutch_cli
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "clutch-cli"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "GitHub tracks your work. Clutch tracks you."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Lay Patel", email = "lay.patel.1313@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
keywords = ["github", "developer", "activity", "cli", "dashboard", "streak"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"typer>=0.12.0",
|
|
26
|
+
"httpx>=0.27.0",
|
|
27
|
+
"rich>=13.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/laypatel13/clutch"
|
|
32
|
+
"Bug Tracker" = "https://github.com/laypatel13/clutch/issues"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
clutch = "clutch_cli.main:app"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["clutch_cli*"]
|