get2work 0.1.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.
@@ -0,0 +1,7 @@
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .get2work/
5
+ *.egg-info/
6
+ dist/
7
+ build/
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: get2work
3
+ Version: 0.1.0
4
+ Summary: The Python library that makes you actually work
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: anthropic>=0.20.0
7
+ Requires-Dist: apscheduler>=3.10.0
8
+ Requires-Dist: gitpython>=3.1.0
9
+ Requires-Dist: psutil>=5.9.0
10
+ Requires-Dist: pygame>=2.5.0
11
+ Requires-Dist: requests>=2.31.0
12
+ Requires-Dist: rich>=13.0.0
13
+ Requires-Dist: typer>=0.9.0
14
+ Requires-Dist: watchdog>=3.0.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # get2work
18
+
19
+ > The Python library that makes you actually work.
20
+
21
+ [![PyPI version](https://badge.fury.io/py/get2work.svg)](https://badge.fury.io/py/get2work)
22
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
23
+
24
+ get2work is a CLI tool that gamifies your coding productivity. It celebrates your commits, roasts your git history, shames you when you're slacking, and makes you actually ship things.
25
+
26
+ ## Features
27
+
28
+ - **Commit celebrations** — animations and sounds every time you commit
29
+ - **Level system** — 8 levels from "Hello World Survivor" to "Linus Would Be Proud"
30
+ - **Streak tracking** — daily commit streaks
31
+ - **Shame notifications** — get roasted if you haven't committed today
32
+ - **AI roasts** — your commit history analyzed and destroyed by AI
33
+ - **Vibe check** — AI mental health diagnosis based on your commits
34
+ - **Peer pressure** — see what other devs are shipping and feel bad
35
+ - **Pomodoro timer** — focus sessions that count toward your level
36
+ - **Accountability receipt** — daily summary of what you actually did
37
+ - **Code funeral** — deleting 100+ lines triggers a funeral
38
+ - **Git blame but personal** — AI roasts each commit individually
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install get2work
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```bash
49
+ # Install the git hook in your repo (do this once per project)
50
+ get2work install
51
+
52
+ # Check your level and stats
53
+ get2work status
54
+
55
+ # Now just commit normally and watch the magic happen
56
+ git commit -m "feat: add something cool"
57
+ ```
58
+
59
+ ## Commands
60
+
61
+ | Command | Description |
62
+ |---|---|
63
+ | `get2work install` | Install git hook in current repo |
64
+ | `get2work status` | Your level, streak, and stats |
65
+ | `get2work roast` | Get your commits roasted by AI |
66
+ | `get2work vibe` | AI mental health diagnosis |
67
+ | `get2work blame` | Git blame but make it personal |
68
+ | `get2work peer <username>` | Stalk a dev on GitHub |
69
+ | `get2work randompeer` | Random dev from the internet |
70
+ | `get2work pomodoro` | Start a pomodoro timer |
71
+ | `get2work shame` | Check if you deserve to be shamed |
72
+ | `get2work receipt` | Your accountability receipt |
73
+ | `get2work start` | Start background watcher |
74
+
75
+ ## AI Features (Optional)
76
+
77
+ The `roast`, `vibe`, and `blame` commands use AI for better results. Without an API key they fall back to hardcoded funny responses.
78
+
79
+ To enable AI roasts set your OpenAI API key:
80
+
81
+ ```bash
82
+ export OPENAI_API_KEY="sk-..."
83
+ ```
84
+
85
+ ## Custom Sounds
86
+
87
+ Replace default sounds with your own by setting environment variables:
88
+
89
+ ```bash
90
+ export GET2WORK_SOUND_CELEBRATE="/path/to/your/sound.mp3"
91
+ export GET2WORK_SOUND_FUNERAL="/path/to/funeral.mp3"
92
+ export GET2WORK_SOUND_SHAME="/path/to/shame.mp3"
93
+ export GET2WORK_SOUND_LEVELUP="/path/to/levelup.mp3"
94
+ ```
95
+
96
+ ## Level System
97
+
98
+ | Level | Name | Commits needed |
99
+ |---|---|---|
100
+ | 1 | Hello World Survivor | 0 |
101
+ | 2 | Tutorial Finisher | 10 |
102
+ | 3 | Functional but Confused | 30 |
103
+ | 4 | It Works Don't Touch It | 60 |
104
+ | 5 | Commits with Confidence (wrongly) | 100 |
105
+ | 6 | git push --force and it worked | 200 |
106
+ | 7 | The Last Line of Defense | 400 |
107
+ | 8 | Linus Would Be Proud | 700 |
108
+
109
+ ## Tutorial
110
+
111
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](COLAB_LINK_HERE)
112
+
113
+ ## Docker
114
+
115
+ ```bash
116
+ docker build -t get2work .
117
+ docker run get2work
118
+ ```
@@ -0,0 +1,102 @@
1
+ # get2work
2
+
3
+ > The Python library that makes you actually work.
4
+
5
+ [![PyPI version](https://badge.fury.io/py/get2work.svg)](https://badge.fury.io/py/get2work)
6
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
7
+
8
+ get2work is a CLI tool that gamifies your coding productivity. It celebrates your commits, roasts your git history, shames you when you're slacking, and makes you actually ship things.
9
+
10
+ ## Features
11
+
12
+ - **Commit celebrations** — animations and sounds every time you commit
13
+ - **Level system** — 8 levels from "Hello World Survivor" to "Linus Would Be Proud"
14
+ - **Streak tracking** — daily commit streaks
15
+ - **Shame notifications** — get roasted if you haven't committed today
16
+ - **AI roasts** — your commit history analyzed and destroyed by AI
17
+ - **Vibe check** — AI mental health diagnosis based on your commits
18
+ - **Peer pressure** — see what other devs are shipping and feel bad
19
+ - **Pomodoro timer** — focus sessions that count toward your level
20
+ - **Accountability receipt** — daily summary of what you actually did
21
+ - **Code funeral** — deleting 100+ lines triggers a funeral
22
+ - **Git blame but personal** — AI roasts each commit individually
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install get2work
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # Install the git hook in your repo (do this once per project)
34
+ get2work install
35
+
36
+ # Check your level and stats
37
+ get2work status
38
+
39
+ # Now just commit normally and watch the magic happen
40
+ git commit -m "feat: add something cool"
41
+ ```
42
+
43
+ ## Commands
44
+
45
+ | Command | Description |
46
+ |---|---|
47
+ | `get2work install` | Install git hook in current repo |
48
+ | `get2work status` | Your level, streak, and stats |
49
+ | `get2work roast` | Get your commits roasted by AI |
50
+ | `get2work vibe` | AI mental health diagnosis |
51
+ | `get2work blame` | Git blame but make it personal |
52
+ | `get2work peer <username>` | Stalk a dev on GitHub |
53
+ | `get2work randompeer` | Random dev from the internet |
54
+ | `get2work pomodoro` | Start a pomodoro timer |
55
+ | `get2work shame` | Check if you deserve to be shamed |
56
+ | `get2work receipt` | Your accountability receipt |
57
+ | `get2work start` | Start background watcher |
58
+
59
+ ## AI Features (Optional)
60
+
61
+ The `roast`, `vibe`, and `blame` commands use AI for better results. Without an API key they fall back to hardcoded funny responses.
62
+
63
+ To enable AI roasts set your OpenAI API key:
64
+
65
+ ```bash
66
+ export OPENAI_API_KEY="sk-..."
67
+ ```
68
+
69
+ ## Custom Sounds
70
+
71
+ Replace default sounds with your own by setting environment variables:
72
+
73
+ ```bash
74
+ export GET2WORK_SOUND_CELEBRATE="/path/to/your/sound.mp3"
75
+ export GET2WORK_SOUND_FUNERAL="/path/to/funeral.mp3"
76
+ export GET2WORK_SOUND_SHAME="/path/to/shame.mp3"
77
+ export GET2WORK_SOUND_LEVELUP="/path/to/levelup.mp3"
78
+ ```
79
+
80
+ ## Level System
81
+
82
+ | Level | Name | Commits needed |
83
+ |---|---|---|
84
+ | 1 | Hello World Survivor | 0 |
85
+ | 2 | Tutorial Finisher | 10 |
86
+ | 3 | Functional but Confused | 30 |
87
+ | 4 | It Works Don't Touch It | 60 |
88
+ | 5 | Commits with Confidence (wrongly) | 100 |
89
+ | 6 | git push --force and it worked | 200 |
90
+ | 7 | The Last Line of Defense | 400 |
91
+ | 8 | Linus Would Be Proud | 700 |
92
+
93
+ ## Tutorial
94
+
95
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](COLAB_LINK_HERE)
96
+
97
+ ## Docker
98
+
99
+ ```bash
100
+ docker build -t get2work .
101
+ docker run get2work
102
+ ```
@@ -0,0 +1 @@
1
+ from get2work import cli
@@ -0,0 +1,114 @@
1
+ import time
2
+ import random
3
+ import os
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+
7
+ console = Console()
8
+
9
+ SOUNDS_DIR = Path(__file__).parent.parent / "sounds"
10
+
11
+ CUSTOM_SOUNDS = {
12
+ "celebrate": os.environ.get("GET2WORK_SOUND_CELEBRATE"),
13
+ "funeral": os.environ.get("GET2WORK_SOUND_FUNERAL"),
14
+ "shame": os.environ.get("GET2WORK_SOUND_SHAME"),
15
+ "levelup": os.environ.get("GET2WORK_SOUND_LEVELUP"),
16
+ "success": os.environ.get("GET2WORK_SOUND_SUCCESS"),
17
+ "error": os.environ.get("GET2WORK_SOUND_ERROR"),
18
+ }
19
+
20
+ DEFAULT_SOUNDS = {
21
+ "celebrate": SOUNDS_DIR / "celebrate.mp3",
22
+ "funeral": SOUNDS_DIR / "funeral.mp3",
23
+ "shame": SOUNDS_DIR / "shame.mp3",
24
+ "levelup": SOUNDS_DIR / "levelup.mp3",
25
+ "success": SOUNDS_DIR / "success.mp3",
26
+ "error": SOUNDS_DIR / "error.mp3",
27
+ }
28
+
29
+ COMMIT_MESSAGES = [
30
+ "YOOOO YOU ACTUALLY COMMITTED 🔥",
31
+ "look at you being productive omg",
32
+ "a commit?? in THIS economy??",
33
+ "slay bestie, that's a commit",
34
+ "the git history thanks you",
35
+ "ok ok ok we're doing this 💪",
36
+ "committed and not quitting, respect",
37
+ "your future self says thanks... maybe",
38
+ ]
39
+
40
+ LEVELUP_MESSAGES = [
41
+ "YOU LEVELED UP ARE YOU KIDDING ME",
42
+ "bro is actually getting good at this",
43
+ "the grindset is REAL",
44
+ "level up achieved, touch grass later",
45
+ "ok we are NOT the same anymore 👑",
46
+ ]
47
+
48
+ FUNERAL_MESSAGES = [
49
+ "rip to those lines of code 💀",
50
+ "they didn't make it...",
51
+ "gone but not forgotten (they were probably bad anyway)",
52
+ "a moment of silence for your deleted code",
53
+ "the codebase is lighter now. emotionally too.",
54
+ ]
55
+
56
+ def play_sound(name: str):
57
+ try:
58
+ import pygame
59
+ custom = CUSTOM_SOUNDS.get(name)
60
+ path = Path(custom) if custom else DEFAULT_SOUNDS.get(name)
61
+
62
+ if not path or not path.exists():
63
+ return
64
+
65
+ pygame.mixer.init()
66
+ pygame.mixer.music.load(str(path))
67
+ pygame.mixer.music.play()
68
+ time.sleep(1.5)
69
+ pygame.mixer.quit()
70
+ except Exception:
71
+ pass
72
+
73
+ def celebrate_commit():
74
+ colors = ["red", "yellow", "green", "cyan", "magenta", "blue"]
75
+ msg = random.choice(COMMIT_MESSAGES)
76
+
77
+ play_sound("celebrate")
78
+ console.print()
79
+ for i in range(3):
80
+ color = random.choice(colors)
81
+ console.print(f" {'🎉' * (i+2)} {msg} {'🎉' * (i+2)}", style=f"bold {color}")
82
+ time.sleep(0.15)
83
+ console.print()
84
+
85
+ def celebrate_levelup(level_name: str):
86
+ msg = random.choice(LEVELUP_MESSAGES)
87
+
88
+ play_sound("levelup")
89
+ console.print()
90
+ console.rule("[bold yellow]LEVEL UP[/bold yellow]")
91
+ console.print(f"\n ⚡ [bold yellow]{msg}[/bold yellow]")
92
+ console.print(f" 👑 [bold cyan]You are now: {level_name}[/bold cyan]\n")
93
+ console.rule("[bold yellow]LEVEL UP[/bold yellow]")
94
+ console.print()
95
+
96
+ def code_funeral():
97
+ msg = random.choice(FUNERAL_MESSAGES)
98
+
99
+ play_sound("funeral")
100
+ console.print()
101
+ console.rule("[bold red]💀 CODE FUNERAL 💀[/bold red]")
102
+ console.print(f"\n [bold red]{msg}[/bold red]")
103
+ console.print(" [dim]deleted: yes. missed: debatable.[/dim]\n")
104
+ console.rule("[bold red]💀 💀 💀[/bold red]")
105
+ console.print()
106
+
107
+ def play_success():
108
+ play_sound("success")
109
+
110
+ def play_error():
111
+ play_sound("error")
112
+
113
+ def play_shame():
114
+ play_sound("shame")
@@ -0,0 +1,227 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from datetime import date
4
+
5
+ app = typer.Typer(help="get2work — stop scrolling, start coding")
6
+ console = Console()
7
+
8
+ @app.command()
9
+ def install():
10
+ """Install get2work hook in your repo 🔧"""
11
+ import os
12
+ import stat
13
+ from pathlib import Path
14
+
15
+ try:
16
+ import git
17
+ repo = git.Repo(".", search_parent_directories=True)
18
+ hooks_dir = Path(repo.git_dir) / "hooks"
19
+ hook_path = hooks_dir / "post-commit"
20
+
21
+ hook_script = """#!/bin/sh
22
+ get2work celebrate-commit
23
+ """
24
+ with open(hook_path, "w") as f:
25
+ f.write(hook_script)
26
+
27
+ st = os.stat(hook_path)
28
+ os.chmod(hook_path, st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
29
+
30
+ console.print("\n [bold green]✅ get2work installed![/bold green]")
31
+ console.print(" [dim]now every commit will be celebrated 🎉[/dim]\n")
32
+
33
+ except Exception as e:
34
+ console.print(f"\n [bold red]❌ error: {e}[/bold red]\n")
35
+
36
+ @app.command()
37
+ def celebrate_commit():
38
+ """Triggered automatically on commit 🎉"""
39
+ from get2work.storage import load, increment
40
+ from get2work.levels import check_levelup
41
+ from get2work.celebrate import celebrate_commit as do_celebrate, celebrate_levelup, code_funeral
42
+ import git
43
+
44
+ try:
45
+ repo = git.Repo(".", search_parent_directories=True)
46
+ commits = list(repo.iter_commits(max_count=1))
47
+ if commits:
48
+ commit = commits[0]
49
+ try:
50
+ stats = commit.stats.total
51
+ lines_added = stats.get("insertions", 0)
52
+ lines_deleted = stats.get("deletions", 0)
53
+ except Exception:
54
+ lines_added = 0
55
+ lines_deleted = 0
56
+
57
+ increment("commits")
58
+ increment("total_lines_added", lines_added)
59
+ increment("total_lines_deleted", lines_deleted)
60
+
61
+ from get2work.watcher import update_streak
62
+ update_streak()
63
+
64
+ do_celebrate()
65
+
66
+ if lines_deleted > 100:
67
+ code_funeral()
68
+
69
+ leveled_up, level_name = check_levelup()
70
+ if leveled_up:
71
+ celebrate_levelup(level_name)
72
+
73
+ except Exception as e:
74
+ pass
75
+
76
+ @app.command()
77
+ def status():
78
+ """Check your level, streak and stats"""
79
+ from get2work.storage import load
80
+ from get2work.levels import get_level, get_next_level
81
+
82
+ data = load()
83
+ commits = data["commits"]
84
+ streak = data["streak"]
85
+ level_num, level_name = get_level(commits)
86
+ next_level = get_next_level(commits)
87
+
88
+ console.print()
89
+ console.rule("[bold cyan]get2work[/bold cyan]")
90
+ console.print(f"\n 👾 [bold cyan]Level {level_num}[/bold cyan] — [yellow]{level_name}[/yellow]")
91
+ console.print(f" 🔥 [bold]Streak:[/bold] {streak} days")
92
+ console.print(f" 📦 [bold]Total commits:[/bold] {commits}")
93
+ console.print(f" 🍅 [bold]Pomodoros:[/bold] {data['pomodoros_completed']}")
94
+ console.print(f" 👀 [bold]Distractions caught:[/bold] {data['distractions_caught']}")
95
+
96
+ if next_level:
97
+ needed = next_level[0] - commits
98
+ console.print(f"\n ⚡ [dim]{needed} commits until [bold]{next_level[1]}[/bold][/dim]")
99
+
100
+ console.print()
101
+
102
+ @app.command()
103
+ def roast():
104
+ """Get your commits roasted 🔥"""
105
+ from get2work.roast import roast_my_commits
106
+ roast_my_commits()
107
+
108
+ @app.command()
109
+ def blame():
110
+ """Git blame but make it personal 🔍"""
111
+ from get2work.roast import git_blame_personal
112
+ git_blame_personal()
113
+
114
+ @app.command()
115
+ def vibe():
116
+ """Get a mental health diagnosis based on your commits 🧠"""
117
+ from get2work.roast import vibe_check
118
+ vibe_check()
119
+
120
+ @app.command()
121
+ def peer(username: str = typer.Argument(..., help="GitHub username to stalk 👀")):
122
+ """See what others are committing and feel bad about yourself 👀"""
123
+ from get2work.peer_pressure import peer_pressure
124
+ peer_pressure(username)
125
+
126
+ @app.command()
127
+ def randompeer():
128
+ """Get shamed by a random dev from the internet 🎲"""
129
+ from get2work.peer_pressure import random_peer
130
+ random_peer()
131
+
132
+ @app.command()
133
+ def distraction():
134
+ """Check if you're slacking right now 👀"""
135
+ from get2work.distraction import distraction_check
136
+ caught = distraction_check()
137
+ if not caught:
138
+ console.print("\n [bold green]✅ no distractions detected[/bold green]")
139
+ console.print(" [dim]surprisingly productive. respect.[/dim]\n")
140
+
141
+
142
+ @app.command()
143
+ def shame():
144
+ """Check if you deserve to be shamed right now 😤"""
145
+ from get2work.watcher import CommitWatcher
146
+ watcher = CommitWatcher()
147
+ watcher.check_shame()
148
+
149
+ @app.command()
150
+ def start():
151
+ """Start get2work in background — watches commits and distractions 👀"""
152
+ import threading
153
+ import time
154
+ from get2work.watcher import CommitWatcher
155
+ from get2work.distraction import start_distraction_watcher
156
+
157
+ console.print("\n [bold green]get2work is watching you 👀[/bold green]")
158
+ console.print(" [dim]commit something or face the consequences[/dim]\n")
159
+
160
+ watcher = CommitWatcher()
161
+
162
+ def shame_loop():
163
+ while True:
164
+ time.sleep(60 * 30)
165
+ watcher.check_shame()
166
+
167
+ def distraction_loop():
168
+ start_distraction_watcher(interval_seconds=120)
169
+
170
+ t1 = threading.Thread(target=shame_loop, daemon=True)
171
+ t2 = threading.Thread(target=distraction_loop, daemon=True)
172
+ t1.start()
173
+ t2.start()
174
+
175
+ try:
176
+ while True:
177
+ time.sleep(1)
178
+ except KeyboardInterrupt:
179
+ console.print("\n [dim]get2work stopped. go touch grass.[/dim]\n")
180
+
181
+ @app.command()
182
+ def pomodoro(minutes: int = typer.Option(25, help="Pomodoro duration in minutes")):
183
+ """Start a pomodoro timer 🍅"""
184
+ import time
185
+ from get2work.storage import increment
186
+
187
+ console.print(f"\n 🍅 [bold green]Pomodoro started — {minutes} minutes[/bold green]")
188
+ console.print(" [dim]focus mode activated, no excuses[/dim]\n")
189
+
190
+ total_seconds = minutes * 60
191
+ with console.status("[green]working...[/green]") as status:
192
+ for remaining in range(total_seconds, 0, -1):
193
+ mins, secs = divmod(remaining, 60)
194
+ status.update(f"[green]⏱ {mins:02d}:{secs:02d} remaining — stay focused[/green]")
195
+ time.sleep(1)
196
+
197
+ increment("pomodoros_completed")
198
+ console.print("\n ✅ [bold green]POMODORO DONE![/bold green]")
199
+ console.print(" [dim]ok now you can touch grass for 5 minutes[/dim]\n")
200
+
201
+ @app.command()
202
+ def receipt():
203
+ """Get your accountability receipt for today 🧾"""
204
+ from get2work.storage import load
205
+ from get2work.levels import get_level
206
+ import datetime
207
+
208
+ data = load()
209
+ now = datetime.datetime.now()
210
+ level_num, level_name = get_level(data["commits"])
211
+
212
+ console.print()
213
+ console.rule("[bold white]🧾 ACCOUNTABILITY RECEIPT[/bold white]")
214
+ console.print(f" [dim]{now.strftime('%Y-%m-%d %H:%M')}[/dim]\n")
215
+ console.print(f" Level: {level_name}")
216
+ console.print(f" Commits: {data['commits']}")
217
+ console.print(f" Streak: {data['streak']} days")
218
+ console.print(f" Pomodoros: {data['pomodoros_completed']}")
219
+ console.print(f" Distractions: {data['distractions_caught']}")
220
+ console.print(f"\n [dim]Lines added: {data['total_lines_added']}[/dim]")
221
+ console.print(f" [dim]Lines deleted: {data['total_lines_deleted']}[/dim]")
222
+ console.print()
223
+ console.rule("[bold white]thanks for coming to get2work[/bold white]")
224
+ console.print()
225
+
226
+ if __name__ == "__main__":
227
+ app()
@@ -0,0 +1,74 @@
1
+ import psutil
2
+ import random
3
+ from rich.console import Console
4
+ from get2work.storage import increment
5
+ from get2work.celebrate import play_shame
6
+
7
+ console = Console()
8
+
9
+ DISTRACTING_APPS = [
10
+ "netflix", "spotify", "discord", "telegram",
11
+ "whatsapp", "instagram", "tiktok", "youtube",
12
+ "twitch", "steam", "epicgames", "roblox",
13
+ "minecraft", "fortnite", "chess", "solitaire",
14
+ "slack", "zoom", "teams",
15
+ ]
16
+
17
+ DISTRACTING_URLS_KEYWORDS = [
18
+ "youtube", "netflix", "twitter", "instagram",
19
+ "tiktok", "reddit", "facebook", "twitch",
20
+ "memes", "buzzfeed", "9gag",
21
+ ]
22
+
23
+ CAUGHT_MESSAGES = [
24
+ "bro really said 'just 5 minutes' 💀",
25
+ "the audacity. the AUDACITY.",
26
+ "we saw that. get back to work.",
27
+ "not you watching {app} instead of coding",
28
+ "your commits are not going to write themselves",
29
+ "caught in 4k. close it.",
30
+ "{app}?? really?? RIGHT NOW??",
31
+ "the repo is crying. {app} can wait.",
32
+ "ok so we're doing THIS instead of shipping features",
33
+ "distraction detected. shame incoming. 🚨",
34
+ ]
35
+
36
+ def get_running_processes() -> list:
37
+ processes = []
38
+ try:
39
+ for proc in psutil.process_iter(['name']):
40
+ try:
41
+ name = proc.info['name'].lower()
42
+ processes.append(name)
43
+ except Exception:
44
+ pass
45
+ except Exception:
46
+ pass
47
+ return processes
48
+
49
+ def check_distractions() -> str | None:
50
+ processes = get_running_processes()
51
+ for proc in processes:
52
+ for app in DISTRACTING_APPS:
53
+ if app in proc:
54
+ return app
55
+ return None
56
+
57
+ def distraction_check():
58
+ caught = check_distractions()
59
+ if caught:
60
+ increment("distractions_caught")
61
+ play_shame()
62
+
63
+ msg = random.choice(CAUGHT_MESSAGES).replace("{app}", caught)
64
+ console.print(f"\n [bold red]🚨 DISTRACTION DETECTED[/bold red]")
65
+ console.print(f" [red]{msg}[/red]\n")
66
+ return True
67
+ return False
68
+
69
+ def start_distraction_watcher(interval_seconds: int = 120):
70
+ import time
71
+ console.print(" [dim]distraction detector active 👀[/dim]")
72
+ while True:
73
+ distraction_check()
74
+ time.sleep(interval_seconds)
@@ -0,0 +1,37 @@
1
+ from get2work.storage import load, save
2
+
3
+ LEVELS = [
4
+ (0, "Hello World Survivor"),
5
+ (10, "Tutorial Finisher"),
6
+ (30, "Functional but Confused"),
7
+ (60, "It Works Don't Touch It"),
8
+ (100, "Commits with Confidence (wrongly)"),
9
+ (200, "git push --force and it worked"),
10
+ (400, "The Last Line of Defense"),
11
+ (700, "Linus Would Be Proud"),
12
+ ]
13
+
14
+ def get_level(commits: int) -> tuple:
15
+ current = (1, LEVELS[0][1])
16
+ for i, (threshold, name) in enumerate(LEVELS):
17
+ if commits >= threshold:
18
+ current = (i + 1, name)
19
+ return current
20
+
21
+ def get_next_level(commits: int) -> tuple | None:
22
+ for threshold, name in LEVELS:
23
+ if commits < threshold:
24
+ return (threshold, name)
25
+ return None
26
+
27
+ def check_levelup() -> tuple:
28
+ data = load()
29
+ commits = data["commits"]
30
+ level_num, level_name = get_level(commits)
31
+ old_level = data.get("level", 1)
32
+
33
+ if level_num > old_level:
34
+ data["level"] = level_num
35
+ save(data)
36
+ return True, level_name
37
+ return False, level_name
@@ -0,0 +1,123 @@
1
+ import random
2
+ import requests
3
+ from rich.console import Console
4
+ from datetime import datetime, timezone
5
+
6
+ console = Console()
7
+
8
+ SARCASTIC_COMMENTS = [
9
+ "and what are YOU doing with your life?",
10
+ "must be nice to actually commit things 👀",
11
+ "imagine being this productive. couldn't be you huh",
12
+ "they said 'i'll do it later' and then did it NOW",
13
+ "not you watching others work while you're here",
14
+ "the dedication... the consistency... the shame you should feel",
15
+ "ok but why are they built different tho",
16
+ "they woke up and chose violence (against procrastination)",
17
+ "this person has never heard of 'i'll do it tomorrow'",
18
+ "ratio'd by someone's commit history 💀",
19
+ "they're just like you but... productive",
20
+ "bro said 'ship it' and actually shipped it",
21
+ ]
22
+
23
+ NO_COMMITS_COMMENTS = [
24
+ "even THEY took a day off. you have no excuse though.",
25
+ "ok they're resting. you should be coding.",
26
+ "slow day for them. not an excuse for you.",
27
+ ]
28
+
29
+ RANDOM_USERNAMES = [
30
+ "antfu", "patak-dev", "sxzz",
31
+ "privatenumber", "jantimon", "nzakas",
32
+ "ljharb", "nicolo-ribaudo", "sheremet-va",
33
+ "yyx990803", "egoist", "pi0",
34
+ "unjs", "Rich-Harris", "sveltejs",
35
+ ]
36
+
37
+ def get_recent_commits(username: str) -> list:
38
+ try:
39
+ url = f"https://api.github.com/users/{username}/events/public"
40
+ response = requests.get(url, timeout=3)
41
+ if response.status_code != 200:
42
+ return []
43
+
44
+ events = response.json()
45
+ commits = []
46
+ for event in events:
47
+ if event.get("type") == "PushEvent":
48
+ payload = event.get("payload", {})
49
+ repo = event.get("repo", {}).get("name", "unknown")
50
+ event_commits = payload.get("commits", [])
51
+ if event_commits:
52
+ for commit in event_commits:
53
+ msg = commit.get("message", "").split("\n")[0]
54
+ if msg:
55
+ commits.append({
56
+ "message": msg,
57
+ "repo": repo,
58
+ "date": event.get("created_at", ""),
59
+ })
60
+ else:
61
+ commits.append({
62
+ "message": "pushed some code 👀",
63
+ "repo": repo,
64
+ "date": event.get("created_at", ""),
65
+ })
66
+ if len(commits) >= 5:
67
+ break
68
+ return commits
69
+
70
+ except Exception:
71
+ return []
72
+
73
+ def format_time_ago(date_str: str) -> str:
74
+ try:
75
+ dt = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
76
+ now = datetime.now(timezone.utc)
77
+ diff = now - dt
78
+ minutes = int(diff.total_seconds() / 60)
79
+
80
+ if minutes < 60:
81
+ return f"{minutes}m ago"
82
+ elif minutes < 1440:
83
+ return f"{minutes // 60}h ago"
84
+ else:
85
+ return f"{minutes // 1440}d ago"
86
+ except Exception:
87
+ return "recently"
88
+
89
+ def _display_commits(username: str, commits: list):
90
+ if not commits:
91
+ comment = random.choice(NO_COMMITS_COMMENTS)
92
+ console.print(f" [dim]no recent commits from {username}[/dim]")
93
+ console.print(f" [yellow]👀 {comment}[/yellow]\n")
94
+ return
95
+
96
+ comment = random.choice(SARCASTIC_COMMENTS)
97
+ console.print(f" [bold]recent commits by [cyan]{username}[/cyan]:[/bold]\n")
98
+
99
+ for c in commits:
100
+ time_ago = format_time_ago(c["date"])
101
+ console.print(f" [green]▸[/green] [white]{c['message']}[/white]")
102
+ console.print(f" [dim]{c['repo']} · {time_ago}[/dim]")
103
+
104
+ console.print(f"\n [bold yellow]👀 {comment}[/bold yellow]\n")
105
+
106
+ def peer_pressure(username: str):
107
+ console.print(f"\n [bold cyan]checking what {username} is up to...[/bold cyan]\n")
108
+ commits = get_recent_commits(username)
109
+ _display_commits(username, commits)
110
+
111
+ def random_peer():
112
+ console.print(f"\n [bold magenta]pulling a random dev from the void...[/bold magenta]\n")
113
+
114
+ usernames = random.sample(RANDOM_USERNAMES, len(RANDOM_USERNAMES))
115
+
116
+ for username in usernames:
117
+ commits = get_recent_commits(username)
118
+ if commits:
119
+ _display_commits(username, commits)
120
+ return
121
+
122
+ console.print(" [dim]all devs are slacking too. rare.[/dim]")
123
+ console.print(" [yellow]👀 even the internet is taking a break. no excuses for you though.[/yellow]\n")
@@ -0,0 +1,181 @@
1
+ import random
2
+ from rich.console import Console
3
+
4
+ console = Console()
5
+
6
+ ROAST_FALLBACK = [
7
+ "your commits look like you're coding with your eyes closed ngl",
8
+ "bro really said 'fix' 5 times in a row. fix WHAT??",
9
+ "committing at 3am again? the grindset is NOT it",
10
+ "your commit history reads like a cry for help",
11
+ "one commit today? one? really? ok.",
12
+ "the way you commit... your tech lead is losing sleep",
13
+ "'update' is not a commit message. try again bestie",
14
+ "your streak died and you didn't even notice 💀",
15
+ "bro codes like they're defusing a bomb at all times",
16
+ "the git log called. it's concerned about you.",
17
+ ]
18
+
19
+ VIBE_FALLBACK = [
20
+ "diagnosis: you're cooked but in a productive way",
21
+ "your commits say 'i am fine' but the timestamps say otherwise",
22
+ "clinical assessment: chaotic good developer with trust issues",
23
+ "you commit like someone who has seen things. dark things.",
24
+ "prognosis: will survive, but the codebase might not",
25
+ "your vibe is 'senior dev in a junior's body' and it shows",
26
+ "mental state: 404 work life balance not found",
27
+ "you're not burnout, you're just... extra crispy",
28
+ ]
29
+
30
+ def get_commits_summary(repo_path: str = ".") -> str:
31
+ try:
32
+ import git
33
+ repo = git.Repo(repo_path, search_parent_directories=True)
34
+ commits = list(repo.iter_commits(max_count=20))
35
+ if not commits:
36
+ return "No commits found"
37
+ summary = []
38
+ for c in commits:
39
+ summary.append(f"- {c.message.strip()} ({c.authored_datetime.strftime('%A %H:%M')})")
40
+ return "\n".join(summary)
41
+ except Exception as e:
42
+ return f"Error reading commits: {e}"
43
+
44
+ def _try_ai_roast(commits_summary: str, mode: str = "roast") -> str | None:
45
+ try:
46
+ from openai import OpenAI
47
+ import os
48
+
49
+ api_key = os.environ.get("OPENAI_API_KEY")
50
+ if not api_key:
51
+ return None
52
+
53
+ client = OpenAI(api_key=api_key)
54
+
55
+ if mode == "roast":
56
+ system = """You are a savage but funny code roaster.
57
+ Roast the developer based on their git commit history.
58
+ Be sarcastic, funny and gen z. Max 4 lines.
59
+ Respond in the same language the commits are written in."""
60
+ else:
61
+ system = """You are a therapist for developers but make it sarcastic.
62
+ Analyze the git commit history and give a mental health diagnosis.
63
+ Be funny, gen z, and dramatic. Max 5 lines.
64
+ Respond in the same language the commits are written in."""
65
+
66
+ response = client.chat.completions.create(
67
+ model="gpt-4o-mini",
68
+ max_tokens=300,
69
+ messages=[
70
+ {"role": "system", "content": system},
71
+ {"role": "user", "content": f"Commits:\n{commits_summary}"}
72
+ ]
73
+ )
74
+ return response.choices[0].message.content
75
+
76
+ except Exception:
77
+ return None
78
+
79
+ def roast_my_commits(repo_path: str = "."):
80
+ console.print("\n [bold magenta]loading your L's...[/bold magenta]\n")
81
+ commits = get_commits_summary(repo_path)
82
+
83
+ result = _try_ai_roast(commits, mode="roast")
84
+ source = "🤖 AI ROAST" if result else "🔥 ROAST"
85
+ if not result:
86
+ result = random.choice(ROAST_FALLBACK)
87
+
88
+ console.print(f" [bold red]{source}:[/bold red]\n")
89
+ console.print(f" [red]{result}[/red]\n")
90
+
91
+ def vibe_check(repo_path: str = "."):
92
+ console.print("\n [bold cyan]analyzing your vibe...[/bold cyan]\n")
93
+ commits = get_commits_summary(repo_path)
94
+
95
+ result = _try_ai_roast(commits, mode="vibe")
96
+ source = "🤖 AI VIBE CHECK" if result else "🧠 VIBE CHECK"
97
+ if not result:
98
+ result = random.choice(VIBE_FALLBACK)
99
+
100
+ console.print(f" [bold cyan]{source}:[/bold cyan]\n")
101
+ console.print(f" [cyan]{result}[/cyan]\n")
102
+
103
+ BLAME_FALLBACK = [
104
+ "this line was written by someone who had given up on life",
105
+ "whoever wrote this was clearly having a bad day. we don't judge. we do judge.",
106
+ "this code works and nobody knows why. not even the author.",
107
+ "written at 2am. you can tell.",
108
+ "the author of this line has since left the company. smart move.",
109
+ "this is either genius or a cry for help. probably both.",
110
+ "no comment. literally, they left no comment. typical.",
111
+ "the person who wrote this knew exactly what they were doing. unfortunately.",
112
+ "this line has survived 3 refactors. it's unkillable. it's a cockroach.",
113
+ "written with the confidence of someone who doesn't have to maintain this.",
114
+ ]
115
+
116
+ def git_blame_personal(repo_path: str = "."):
117
+ import git
118
+
119
+ console.print("\n [bold yellow]analyzing who to blame...[/bold yellow]\n")
120
+
121
+ try:
122
+ repo = git.Repo(repo_path, search_parent_directories=True)
123
+ commits = list(repo.iter_commits(max_count=10))
124
+
125
+ if not commits:
126
+ console.print(" [dim]no commits to blame. you're safe. for now.[/dim]\n")
127
+ return
128
+
129
+ blame_data = []
130
+ for commit in commits[:5]:
131
+ blame_data.append({
132
+ "author": commit.author.name,
133
+ "message": commit.message.strip().split("\n")[0],
134
+ "date": commit.authored_datetime.strftime("%A %H:%M"),
135
+ "files": list(commit.stats.files.keys())[:2],
136
+ })
137
+
138
+ result = _try_ai_blame(blame_data)
139
+ if not result:
140
+ import random
141
+ console.print(f" [bold yellow]🔍 GIT BLAME BUT MAKE IT PERSONAL:[/bold yellow]\n")
142
+ for item in blame_data:
143
+ comment = random.choice(BLAME_FALLBACK)
144
+ console.print(f" [white]commit:[/white] [dim]{item['message']}[/dim]")
145
+ console.print(f" [yellow]→ {comment}[/yellow]\n")
146
+ else:
147
+ console.print(f" [bold yellow]🔍 GIT BLAME BUT MAKE IT PERSONAL:[/bold yellow]\n")
148
+ console.print(f" [yellow]{result}[/yellow]\n")
149
+
150
+ except Exception as e:
151
+ console.print(f" [red]error: {e}[/red]\n")
152
+
153
+ def _try_ai_blame(blame_data: list) -> str | None:
154
+ try:
155
+ from openai import OpenAI
156
+ import os
157
+
158
+ api_key = os.environ.get("OPENAI_API_KEY")
159
+ if not api_key:
160
+ return None
161
+
162
+ client = OpenAI(api_key=api_key)
163
+
164
+ system = """You are a savage code historian.
165
+ You analyze git commits and roast each one personally.
166
+ For each commit, make a personal comment about the author's life choices.
167
+ Be funny, gen z, sarcastic. One line per commit max.
168
+ Respond in the same language the commits are written in."""
169
+
170
+ response = client.chat.completions.create(
171
+ model="gpt-4o-mini",
172
+ max_tokens=400,
173
+ messages=[
174
+ {"role": "system", "content": system},
175
+ {"role": "user", "content": f"Blame these commits:\n{str(blame_data)}"}
176
+ ]
177
+ )
178
+ return response.choices[0].message.content
179
+
180
+ except Exception:
181
+ return None
@@ -0,0 +1,38 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ STORAGE_PATH = Path.home() / ".get2work" / "data.json"
5
+
6
+ DEFAULT_DATA = {
7
+ "commits": 0,
8
+ "streak": 0,
9
+ "last_commit_date": None,
10
+ "level": 1,
11
+ "total_lines_added": 0,
12
+ "total_lines_deleted": 0,
13
+ "pomodoros_completed": 0,
14
+ "distractions_caught": 0,
15
+ }
16
+
17
+ def load() -> dict:
18
+ if not STORAGE_PATH.exists():
19
+ STORAGE_PATH.parent.mkdir(parents=True, exist_ok=True)
20
+ save(DEFAULT_DATA)
21
+ return DEFAULT_DATA.copy()
22
+ with open(STORAGE_PATH) as f:
23
+ return json.load(f)
24
+
25
+ def save(data: dict):
26
+ STORAGE_PATH.parent.mkdir(parents=True, exist_ok=True)
27
+ with open(STORAGE_PATH, "w") as f:
28
+ json.dump(data, f, indent=2)
29
+
30
+ def increment(key: str, amount: int = 1):
31
+ data = load()
32
+ data[key] = data.get(key, 0) + amount
33
+ save(data)
34
+
35
+ def update(key: str, value):
36
+ data = load()
37
+ data[key] = value
38
+ save(data)
@@ -0,0 +1,172 @@
1
+ import time
2
+ import random
3
+ import git
4
+ from datetime import date, datetime
5
+ from rich.console import Console
6
+ from get2work.storage import load, save, increment, update
7
+ from get2work.levels import check_levelup
8
+ from get2work.celebrate import celebrate_commit, celebrate_levelup, code_funeral, play_shame
9
+
10
+ console = Console()
11
+
12
+ SHAME_MESSAGES_LOW = [
13
+ "hey... everything ok? no commits today 👀",
14
+ "sooo are we coding or just vibing?",
15
+ "the repo is lonely. just saying.",
16
+ "no pressure but... commit something maybe?",
17
+ "gentle reminder that you exist and so does git",
18
+ ]
19
+
20
+ SHAME_MESSAGES_MED = [
21
+ "your streak is in danger bro 😬",
22
+ "bro really said 'i'll commit later' huh",
23
+ "the codebase misses you. or maybe it doesn't. hard to tell.",
24
+ "ok so we're just not committing today? ok.",
25
+ "your github is looking a little dry rn 💀",
26
+ ]
27
+
28
+ SHAME_MESSAGES_HIGH = [
29
+ "I'M NOT MAD I'M JUST DISAPPOINTED",
30
+ "THE AUDACITY OF NOT COMMITTING",
31
+ "at this point are you even a developer",
32
+ "your github is a desert. a wasteland. nothing.",
33
+ "bro said 'i'm a developer' and then didn't develop 💀",
34
+ "the commit history called. it's filing a missing persons report.",
35
+ ]
36
+
37
+ def get_shame_message(hours_since_commit: int) -> str:
38
+ if hours_since_commit < 4:
39
+ return random.choice(SHAME_MESSAGES_LOW)
40
+ elif hours_since_commit < 8:
41
+ return random.choice(SHAME_MESSAGES_MED)
42
+ else:
43
+ return random.choice(SHAME_MESSAGES_HIGH)
44
+
45
+ class CommitWatcher:
46
+ def __init__(self, repo_path: str = "."):
47
+ self.repo_path = repo_path
48
+ self.last_commit = None
49
+ self._get_last_commit()
50
+
51
+ def _get_last_commit(self):
52
+ try:
53
+ repo = git.Repo(self.repo_path, search_parent_directories=True)
54
+ commits = list(repo.iter_commits(max_count=1))
55
+ if commits:
56
+ self.last_commit = commits[0].hexsha
57
+ except Exception:
58
+ pass
59
+
60
+ def check_new_commit(self):
61
+ try:
62
+ repo = git.Repo(self.repo_path, search_parent_directories=True)
63
+ commits = list(repo.iter_commits(max_count=1))
64
+ if not commits:
65
+ return
66
+ latest = commits[0]
67
+ if latest.hexsha == self.last_commit:
68
+ return
69
+ self.last_commit = latest.hexsha
70
+ self._handle_new_commit(repo, latest)
71
+ except Exception:
72
+ pass
73
+
74
+ def _handle_new_commit(self, repo, commit):
75
+ try:
76
+ stats = commit.stats.total
77
+ lines_added = stats.get("insertions", 0)
78
+ lines_deleted = stats.get("deletions", 0)
79
+ except Exception:
80
+ lines_added = 0
81
+ lines_deleted = 0
82
+
83
+ increment("commits")
84
+ increment("total_lines_added", lines_added)
85
+ increment("total_lines_deleted", lines_deleted)
86
+
87
+ self._update_streak()
88
+ celebrate_commit()
89
+
90
+ if lines_deleted > 100:
91
+ code_funeral()
92
+
93
+ leveled_up, level_name = check_levelup()
94
+ if leveled_up:
95
+ celebrate_levelup(level_name)
96
+
97
+ def _update_streak(self):
98
+ data = load()
99
+ today = str(date.today())
100
+ last = data.get("last_commit_date")
101
+
102
+ if last == today:
103
+ return
104
+
105
+ if last is None:
106
+ data["streak"] = 1
107
+ else:
108
+ last_date = datetime.strptime(last, "%Y-%m-%d").date()
109
+ diff = (date.today() - last_date).days
110
+ if diff == 1:
111
+ data["streak"] = data.get("streak", 0) + 1
112
+ else:
113
+ data["streak"] = 1
114
+
115
+ data["last_commit_date"] = today
116
+ save(data)
117
+
118
+ def check_shame(self):
119
+ data = load()
120
+ last = data.get("last_commit_date")
121
+ today = str(date.today())
122
+
123
+ if last == today:
124
+ console.print("\n [bold green]✅ you committed today. we're proud.[/bold green]")
125
+ console.print(" [dim]don't ruin it.[/dim]\n")
126
+ return
127
+
128
+ if last is None:
129
+ hours = 24
130
+ else:
131
+ last_date = datetime.strptime(last, "%Y-%m-%d")
132
+ hours = int((datetime.now() - last_date).total_seconds() / 3600)
133
+
134
+ msg = get_shame_message(hours)
135
+ play_shame()
136
+ console.print(f"\n [bold red]⚠️ {msg}[/bold red]\n")
137
+
138
+ def update_streak():
139
+ data = load()
140
+ today = str(date.today())
141
+ last = data.get("last_commit_date")
142
+
143
+ if last == today:
144
+ return
145
+
146
+ if last is None:
147
+ data["streak"] = 1
148
+ else:
149
+ last_date = datetime.strptime(last, "%Y-%m-%d").date()
150
+ diff = (date.today() - last_date).days
151
+ if diff == 1:
152
+ data["streak"] = data.get("streak", 0) + 1
153
+ else:
154
+ data["streak"] = 1
155
+
156
+ data["last_commit_date"] = today
157
+ save(data)
158
+
159
+ def start_watching(repo_path: str = "."):
160
+ watcher = CommitWatcher(repo_path)
161
+ console.print("\n [bold green]get2work is watching you 👀[/bold green]")
162
+ console.print(" [dim]commit something or face the consequences[/dim]\n")
163
+
164
+ check_interval = 0
165
+ while True:
166
+ watcher.check_new_commit()
167
+ check_interval += 1
168
+
169
+ if check_interval % 180 == 0:
170
+ watcher.check_shame()
171
+
172
+ time.sleep(10)
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "get2work"
7
+ version = "0.1.0"
8
+ description = "The Python library that makes you actually work"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "typer>=0.9.0",
13
+ "rich>=13.0.0",
14
+ "gitpython>=3.1.0",
15
+ "watchdog>=3.0.0",
16
+ "psutil>=5.9.0",
17
+ "anthropic>=0.20.0",
18
+ "requests>=2.31.0",
19
+ "apscheduler>=3.10.0",
20
+ "pygame>=2.5.0",
21
+ ]
22
+
23
+ [project.scripts]
24
+ get2work = "get2work.cli:app"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["get2work"]
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file