wtf-dev 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,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Environment variables
13
+ .env
@@ -0,0 +1 @@
1
+ 3.11
wtf_dev-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: wtf-dev
3
+ Version: 0.1.0
4
+ Summary: What did I work on? A snarky standup generator.
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: inquirerpy>=0.3.4
7
+ Requires-Dist: pydantic-settings>=2.12.0
8
+ Requires-Dist: pydantic>=2.12.5
9
+ Requires-Dist: pyperclip>=1.11.0
10
+ Requires-Dist: python-dotenv>=1.2.1
11
+ Requires-Dist: requests>=2.32.5
12
+ Requires-Dist: rich>=14.3.1
13
+ Requires-Dist: typer>=0.21.1
14
+ Description-Content-Type: text/markdown
15
+
16
+ # wtf-dev
17
+
18
+ A CLI tool that tells you what you worked on - with personality.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install wtf-dev
24
+ ```
25
+
26
+ ## Setup
27
+
28
+ ```bash
29
+ wtf setup
30
+ ```
31
+
32
+ This will prompt for your OpenRouter API key and let you pick a model.
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ # what did I do today?
38
+ wtf
39
+
40
+ # look back N days
41
+ wtf --days 3
42
+
43
+ # only current repo
44
+ wtf --here
45
+
46
+ # copy to clipboard
47
+ wtf --copy
48
+ ```
49
+
50
+ ## Features
51
+
52
+ - **Standup summary** - LLM-generated summary of your commits
53
+ - **WIP tracking** - Shows uncommitted changes + what you're currently working on
54
+ - **Streak counter** - Track your commit streak
55
+ - **Late night detection** - Spots those 2am coding sessions
56
+ - **Branch context** - Shows which branches you touched
57
+ - **History** - View past standups with `wtf --history`
58
+ - **Cost tracking** - Track API spending with `wtf --spending`
59
+
60
+ ## Output
61
+
62
+ ```
63
+ PREVIOUSLY ON YOUR CODE... Feb 02, 2026
64
+ * 5 day streak
65
+ ────────────────────────────────────────────────────────────
66
+
67
+ ai-platform (main) ─── 2 commits
68
+ ├─ feat(sdr): add langsmith tracing
69
+ └─ feat(sdr): add automatic follow-up
70
+
71
+ [wip]
72
+ ai-platform ─── 3 files changed
73
+ ├─ M src/api/routes.py
74
+ └─ A src/new_feature.py
75
+
76
+ ────────────────────────────────────────────────────────────
77
+
78
+ Added LangSmith tracing and automatic follow-up for stale
79
+ conversations in the SDR pipeline.
80
+
81
+ Currently working on: Adding new API routes for validation.
82
+
83
+ Two features down, infinite bugs to go.
84
+ ```
85
+
86
+ ## Flags
87
+
88
+ | Flag | Short | Description |
89
+ |------|-------|-------------|
90
+ | `--dir PATH` | `-d` | Scan a specific directory |
91
+ | `--here` | `-H` | Only current repo |
92
+ | `--days N` | `-n` | Look back N days (default: 1) |
93
+ | `--author NAME` | `-a` | Filter by author |
94
+ | `--copy` | `-c` | Copy to clipboard |
95
+ | `--history` | | View past standups |
96
+ | `--spending` | | Show API costs |
97
+ | `--json` | | Output as JSON |
@@ -0,0 +1,82 @@
1
+ # wtf-dev
2
+
3
+ A CLI tool that tells you what you worked on - with personality.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install wtf-dev
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ wtf setup
15
+ ```
16
+
17
+ This will prompt for your OpenRouter API key and let you pick a model.
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ # what did I do today?
23
+ wtf
24
+
25
+ # look back N days
26
+ wtf --days 3
27
+
28
+ # only current repo
29
+ wtf --here
30
+
31
+ # copy to clipboard
32
+ wtf --copy
33
+ ```
34
+
35
+ ## Features
36
+
37
+ - **Standup summary** - LLM-generated summary of your commits
38
+ - **WIP tracking** - Shows uncommitted changes + what you're currently working on
39
+ - **Streak counter** - Track your commit streak
40
+ - **Late night detection** - Spots those 2am coding sessions
41
+ - **Branch context** - Shows which branches you touched
42
+ - **History** - View past standups with `wtf --history`
43
+ - **Cost tracking** - Track API spending with `wtf --spending`
44
+
45
+ ## Output
46
+
47
+ ```
48
+ PREVIOUSLY ON YOUR CODE... Feb 02, 2026
49
+ * 5 day streak
50
+ ────────────────────────────────────────────────────────────
51
+
52
+ ai-platform (main) ─── 2 commits
53
+ ├─ feat(sdr): add langsmith tracing
54
+ └─ feat(sdr): add automatic follow-up
55
+
56
+ [wip]
57
+ ai-platform ─── 3 files changed
58
+ ├─ M src/api/routes.py
59
+ └─ A src/new_feature.py
60
+
61
+ ────────────────────────────────────────────────────────────
62
+
63
+ Added LangSmith tracing and automatic follow-up for stale
64
+ conversations in the SDR pipeline.
65
+
66
+ Currently working on: Adding new API routes for validation.
67
+
68
+ Two features down, infinite bugs to go.
69
+ ```
70
+
71
+ ## Flags
72
+
73
+ | Flag | Short | Description |
74
+ |------|-------|-------------|
75
+ | `--dir PATH` | `-d` | Scan a specific directory |
76
+ | `--here` | `-H` | Only current repo |
77
+ | `--days N` | `-n` | Look back N days (default: 1) |
78
+ | `--author NAME` | `-a` | Filter by author |
79
+ | `--copy` | `-c` | Copy to clipboard |
80
+ | `--history` | | View past standups |
81
+ | `--spending` | | Show API costs |
82
+ | `--json` | | Output as JSON |
wtf_dev-0.1.0/WTF.md ADDED
@@ -0,0 +1,128 @@
1
+ # WTF - What The (F)iles Did I Work On?
2
+
3
+ A CLI tool that tells you what you worked on yesterday - with personality.
4
+
5
+ ## What It Does
6
+
7
+ `wtf` scans your git repositories, finds your recent commits, and generates a standup-ready summary with snarky commentary. No more staring at `git log` trying to remember what you did.
8
+
9
+ ```bash
10
+ $ wtf
11
+
12
+ 🎬 PREVIOUSLY ON YOUR CODE...
13
+
14
+ ┌──────────────────────────────────────────────────────────────┐
15
+ │ 📁 api-service (4 commits) │
16
+ ├──────────────────────────────────────────────────────────────┤
17
+ │ • Fixed auth bug │
18
+ │ • Fixed auth bug again │
19
+ │ • Actually fixed auth bug │
20
+ │ • Why is auth so hard │
21
+ │ │
22
+ │ 💬 Four commits, one bug. The bug is winning. │
23
+ └──────────────────────────────────────────────────────────────┘
24
+
25
+ 📋 STANDUP READY:
26
+ "Yesterday I mass mass fixed the auth bug in api-service. It took a few
27
+ attempts but we got there."
28
+
29
+ [c] Copy to clipboard
30
+ ```
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install wtf-cli
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```bash
41
+ # Basic - scan current directory for all repos
42
+ wtf
43
+
44
+ # Only current repo (not recursive)
45
+ wtf --here
46
+
47
+ # Scan specific directory
48
+ wtf --dir ~/projects
49
+
50
+ # Look back N days
51
+ wtf --days 3
52
+
53
+ # Copy standup to clipboard automatically
54
+ wtf --copy
55
+
56
+ # Different output styles
57
+ wtf --style roast # Extra snarky
58
+ wtf --style haiku # Poetic mode
59
+ wtf --style corporate # Buzzword bingo
60
+
61
+ # Use AI for smarter commentary (requires API key)
62
+ wtf --ai
63
+ ```
64
+
65
+ ## Flags
66
+
67
+ | Flag | Short | Description |
68
+ |------|-------|-------------|
69
+ | `--here` | `-h` | Only scan current repo, not subdirectories |
70
+ | `--dir PATH` | `-d` | Scan a specific directory |
71
+ | `--days N` | `-n` | Look back N days (default: 1, or since Friday if Monday) |
72
+ | `--author NAME` | `-a` | Filter by author (default: git config user.name) |
73
+ | `--copy` | `-c` | Copy standup summary to clipboard |
74
+ | `--style STYLE` | `-s` | Output style: `normal`, `roast`, `haiku`, `corporate` |
75
+ | `--ai` | | Use LLM for smarter roasts (requires OPENROUTER_API_KEY) |
76
+ | `--json` | | Output as JSON (for piping to other tools) |
77
+
78
+ ## Configuration
79
+
80
+ Set your API key for AI mode:
81
+
82
+ ```bash
83
+ export OPENROUTER_API_KEY="your-key-here"
84
+ ```
85
+
86
+ Or create `~/.wtf/config.json`:
87
+
88
+ ```json
89
+ {
90
+ "default_style": "roast",
91
+ "auto_copy": true,
92
+ "ai_model": "anthropic/claude-3-haiku"
93
+ }
94
+ ```
95
+
96
+ ## What It Detects
97
+
98
+ - **Vague commits**: "WIP", "fix", "update" → roasts you
99
+ - **Repeated fixes**: Same bug fixed 3 times → notes the struggle
100
+ - **Late night coding**: Commits after midnight → comments on your sleep schedule
101
+ - **Same file touched repeatedly**: Sign of a tricky problem
102
+ - **Empty commit messages**: Shame.
103
+
104
+ ## Examples
105
+
106
+ ```bash
107
+ # Monday morning - shows everything since Friday
108
+ $ wtf
109
+ 🎬 PREVIOUSLY ON YOUR CODE... (Fri-Sun)
110
+
111
+ # Quick standup, copy to clipboard
112
+ $ wtf -c
113
+ ✅ Copied to clipboard!
114
+
115
+ # Extra roasty
116
+ $ wtf --style roast
117
+ 💬 "You mass mass mass mass made 12 commits and mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass 8 of them say 'fix'. Are you okay?"
118
+
119
+ # Haiku mode
120
+ $ wtf --style haiku
121
+ Auth bug defeated
122
+ Four commits mass mass to find the truth
123
+ Sleep comes at last
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,14 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
6
+
7
+ # openrouter config
8
+ openrouter_api_key: str = ""
9
+ openrouter_url: str = "https://openrouter.ai/api/v1/chat/completions"
10
+ wtf_model: str = "openai/gpt-oss-120b"
11
+ wtf_provider: str = "deepinfra/fp4"
12
+
13
+
14
+ settings = Settings()
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "wtf-dev"
3
+ version = "0.1.0"
4
+ description = "What did I work on? A snarky standup generator."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "InquirerPy>=0.3.4",
9
+ "pydantic>=2.12.5",
10
+ "pydantic-settings>=2.12.0",
11
+ "pyperclip>=1.11.0",
12
+ "python-dotenv>=1.2.1",
13
+ "requests>=2.32.5",
14
+ "rich>=14.3.1",
15
+ "typer>=0.21.1",
16
+ ]
17
+
18
+ [project.scripts]
19
+ wtf = "src.cli:app"
20
+
21
+ [tool.uv]
22
+ package = true
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "pytest>=9.0.2",
31
+ "pytest-mock>=3.15.1",
32
+ ]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src"]
36
+
37
+ [tool.pytest.ini_options]
38
+ pythonpath = ["."]
File without changes
@@ -0,0 +1,246 @@
1
+ import json
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import pyperclip
7
+ import typer
8
+
9
+ from . import formatter, storage
10
+ from .git import (
11
+ find_git_repos,
12
+ get_commit_streak,
13
+ get_current_branch,
14
+ get_git_commits,
15
+ get_git_diff,
16
+ get_git_diff_stat,
17
+ get_git_user,
18
+ )
19
+ from .llm import analyze_commits, get_model
20
+ from .models import Commit, RepoSummary, StandupResult, TimeStats, WipSummary
21
+
22
+ app = typer.Typer(add_completion=False, invoke_without_command=True)
23
+
24
+
25
+ @app.command()
26
+ def setup():
27
+ from .setup import run_setup
28
+
29
+ run_setup()
30
+
31
+
32
+ @app.callback(invoke_without_command=True)
33
+ def main(
34
+ ctx: typer.Context,
35
+ dir: Optional[Path] = typer.Option(None, "--dir", "-d"),
36
+ here: bool = typer.Option(False, "--here", "-H"),
37
+ days: int = typer.Option(1, "--days", "-n"),
38
+ author: Optional[str] = typer.Option(None, "--author", "-a"),
39
+ copy: bool = typer.Option(False, "--copy", "-c"),
40
+ spending: bool = typer.Option(False, "--spending"),
41
+ history: bool = typer.Option(False, "--history"),
42
+ json_out: bool = typer.Option(False, "--json"),
43
+ ):
44
+ # if a subcommand was invoked, skip main logic
45
+ if ctx.invoked_subcommand is not None:
46
+ return
47
+
48
+ # check if configured, run setup if not
49
+ if not storage.is_configured():
50
+ formatter.console.print("[yellow]First time? Let's set up wtf.[/yellow]")
51
+ from .setup import run_setup
52
+
53
+ run_setup()
54
+ return
55
+
56
+ # handle --spending
57
+ if spending:
58
+ total = storage.get_total_spent()
59
+ formatter.render_spending(total)
60
+ return
61
+
62
+ # handle --history
63
+ if history:
64
+ past = storage.load_history()
65
+ for item in past:
66
+ formatter.console.print(f"[dim]{item.generated_at}[/dim]")
67
+ formatter.console.print(item.llm_response.summary)
68
+ formatter.console.print()
69
+ return
70
+
71
+ # main flow
72
+ scan_path = str(dir) if dir else "."
73
+ git_author = author or get_git_user() or "unknown"
74
+
75
+ # handle monday (show fri-sun)
76
+ if datetime.now().weekday() == 0:
77
+ days = max(days, 3)
78
+
79
+ # find repos and commits
80
+ if here:
81
+ repos = [scan_path]
82
+ else:
83
+ repos = find_git_repos(scan_path)
84
+
85
+ summaries = []
86
+ wip_summaries = []
87
+ all_commits = []
88
+
89
+ for repo_path in repos:
90
+ commits = get_commits(repo_path, git_author, f"{days} days ago")
91
+ branch = get_current_branch(repo_path)
92
+
93
+ if commits:
94
+ summaries.append(
95
+ RepoSummary(
96
+ name=Path(repo_path).name,
97
+ path=repo_path,
98
+ commits=commits,
99
+ branch=branch,
100
+ )
101
+ )
102
+ all_commits.extend(commits)
103
+
104
+ # gather wip (uncommitted changes)
105
+ diff_stat = get_git_diff_stat(repo_path)
106
+ if diff_stat:
107
+ diff = get_git_diff(repo_path)
108
+ wip_summaries.append(
109
+ WipSummary(
110
+ repo_name=Path(repo_path).name,
111
+ files_changed=diff_stat,
112
+ diff_preview=diff[:2000],
113
+ )
114
+ )
115
+
116
+ if not summaries and not wip_summaries:
117
+ formatter.console.print("[yellow]No commits found.[/yellow]")
118
+ raise typer.Exit()
119
+
120
+ # calculate time stats
121
+ time_stats = calculate_time_stats(all_commits)
122
+
123
+ # get streak
124
+ streak = get_commit_streak(scan_path, git_author) if summaries else 0
125
+
126
+ # call llm
127
+ commits_text = format_for_llm(summaries) if summaries else "No commits."
128
+ diff_text = format_wip_for_llm(wip_summaries) if wip_summaries else None
129
+
130
+ try:
131
+ llm_response, cost = analyze_commits(commits_text, diff_text)
132
+ storage.add_spending(cost, get_model())
133
+ except Exception as e:
134
+ formatter.console.print(f"[red]LLM error: {e}[/red]")
135
+ raise typer.Exit(1)
136
+
137
+ # build result
138
+ result = StandupResult(
139
+ repos=summaries,
140
+ llm_response=llm_response,
141
+ generated_at=datetime.now(),
142
+ cost_usd=cost,
143
+ wip=wip_summaries,
144
+ time_stats=time_stats,
145
+ streak=streak,
146
+ )
147
+
148
+ # save to history
149
+ storage.save_standup(result)
150
+
151
+ # output
152
+ if json_out:
153
+ print(
154
+ json.dumps(
155
+ {
156
+ "summary": result.llm_response.summary,
157
+ "roast": result.llm_response.roast,
158
+ "repos": [
159
+ {"name": r.name, "commits": len(r.commits)} for r in summaries
160
+ ],
161
+ },
162
+ indent=2,
163
+ )
164
+ )
165
+ else:
166
+ formatter.render(result)
167
+
168
+ # copy to clipboard
169
+ if copy:
170
+ pyperclip.copy(result.llm_response.summary)
171
+ formatter.render_copied()
172
+
173
+
174
+ def get_commits(repo_path: str, author: str, since: str) -> list[Commit]:
175
+ raw = get_git_commits(repo_path, author, since)
176
+ if not raw:
177
+ return []
178
+
179
+ commits = []
180
+ for line in raw.split("\n"):
181
+ if "|" in line:
182
+ parts = line.split("|")
183
+ commits.append(
184
+ Commit(
185
+ hash=parts[0],
186
+ message=parts[1],
187
+ date=parts[2] if len(parts) > 2 else "",
188
+ time=parts[3] if len(parts) > 3 else "",
189
+ repo_name=Path(repo_path).name,
190
+ )
191
+ )
192
+ return commits
193
+
194
+
195
+ def format_for_llm(summaries: list[RepoSummary]) -> str:
196
+ lines = []
197
+ for s in summaries:
198
+ lines.append(f"\n{s.name} ({len(s.commits)} commits):")
199
+ for c in s.commits:
200
+ lines.append(f" - {c.message}")
201
+ return "\n".join(lines)
202
+
203
+
204
+ def format_wip_for_llm(wip_summaries: list[WipSummary]) -> str:
205
+ lines = []
206
+ for wip in wip_summaries:
207
+ lines.append(f"\n{wip.repo_name} ({len(wip.files_changed)} files changed):")
208
+ for f in wip.files_changed[:10]:
209
+ lines.append(f" {f}")
210
+ if wip.diff_preview:
211
+ lines.append(f"\nDiff preview:\n{wip.diff_preview[:1500]}")
212
+ return "\n".join(lines)
213
+
214
+
215
+ def calculate_time_stats(commits: list[Commit]) -> TimeStats:
216
+ if not commits:
217
+ return TimeStats()
218
+
219
+ late_night = 0
220
+ early_morning = 0
221
+
222
+ for c in commits:
223
+ if c.time:
224
+ try:
225
+ # time is in ISO format like 2026-02-02T23:45:00+05:30
226
+ hour = int(c.time[11:13])
227
+ if hour >= 22 or hour < 5:
228
+ late_night += 1
229
+ elif hour < 7:
230
+ early_morning += 1
231
+ except (ValueError, IndexError):
232
+ pass
233
+
234
+ # estimate hours: rough heuristic (15 min per commit minimum)
235
+ estimated_hours = len(commits) * 0.25
236
+
237
+ return TimeStats(
238
+ total_commits=len(commits),
239
+ late_night_commits=late_night,
240
+ early_morning_commits=early_morning,
241
+ estimated_hours=estimated_hours,
242
+ )
243
+
244
+
245
+ if __name__ == "__main__":
246
+ app()