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.
- wtf_dev-0.1.0/.gitignore +13 -0
- wtf_dev-0.1.0/.python-version +1 -0
- wtf_dev-0.1.0/PKG-INFO +97 -0
- wtf_dev-0.1.0/README.md +82 -0
- wtf_dev-0.1.0/WTF.md +128 -0
- wtf_dev-0.1.0/config.py +14 -0
- wtf_dev-0.1.0/pyproject.toml +38 -0
- wtf_dev-0.1.0/src/__init__.py +0 -0
- wtf_dev-0.1.0/src/cli.py +246 -0
- wtf_dev-0.1.0/src/formatter.py +111 -0
- wtf_dev-0.1.0/src/git.py +187 -0
- wtf_dev-0.1.0/src/llm.py +110 -0
- wtf_dev-0.1.0/src/models.py +47 -0
- wtf_dev-0.1.0/src/setup.py +106 -0
- wtf_dev-0.1.0/src/storage.py +85 -0
- wtf_dev-0.1.0/tests/__init__.py +0 -0
- wtf_dev-0.1.0/tests/conftest.py +5 -0
- wtf_dev-0.1.0/tests/test_cli.py +64 -0
- wtf_dev-0.1.0/tests/test_git.py +51 -0
- wtf_dev-0.1.0/tests/test_llm.py +54 -0
- wtf_dev-0.1.0/tests/test_storage.py +71 -0
- wtf_dev-0.1.0/uv.lock +517 -0
- wtf_dev-0.1.0/why-wtf-is-useful.md +162 -0
wtf_dev-0.1.0/.gitignore
ADDED
|
@@ -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 |
|
wtf_dev-0.1.0/README.md
ADDED
|
@@ -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
|
wtf_dev-0.1.0/config.py
ADDED
|
@@ -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
|
wtf_dev-0.1.0/src/cli.py
ADDED
|
@@ -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()
|