gitdude 1.0.0__py3-none-any.whl
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.
- gitdude-1.0.0.dist-info/METADATA +305 -0
- gitdude-1.0.0.dist-info/RECORD +10 -0
- gitdude-1.0.0.dist-info/WHEEL +4 -0
- gitdude-1.0.0.dist-info/entry_points.txt +2 -0
- gitwise/__init__.py +5 -0
- gitwise/ai.py +148 -0
- gitwise/config.py +106 -0
- gitwise/git_ops.py +235 -0
- gitwise/main.py +826 -0
- gitwise/utils.py +150 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitdude
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AI-powered Git workflow assistant — commit, review, recover, and more with a single command
|
|
5
|
+
Project-URL: Homepage, https://github.com/utkarshgupta188/gitdude
|
|
6
|
+
Project-URL: Repository, https://github.com/utkarshgupta188/gitdude
|
|
7
|
+
Project-URL: Documentation, https://github.com/utkarshgupta188/gitdude#readme
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/utkarshgupta188/gitdude/issues
|
|
9
|
+
Author: GitDude Contributors
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: ai,cli,developer-tools,gemini,git,groq,ollama,openai
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: gitpython>=3.1.40
|
|
26
|
+
Requires-Dist: google-generativeai>=0.7.0
|
|
27
|
+
Requires-Dist: groq>=0.9.0
|
|
28
|
+
Requires-Dist: ollama>=0.2.0
|
|
29
|
+
Requires-Dist: openai>=1.30.0
|
|
30
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
31
|
+
Requires-Dist: rich>=13.7.0
|
|
32
|
+
Requires-Dist: typer[all]>=0.12.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# GitDude 🧠
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/gitwise/)
|
|
43
|
+
[](https://www.python.org/downloads/)
|
|
44
|
+
[](https://opensource.org/licenses/MIT)
|
|
45
|
+
[](https://pypi.org/project/gitwise/)
|
|
46
|
+
|
|
47
|
+
**GitDude is an AI-powered Git workflow assistant that turns plain English into git actions — commit, review, recover, and understand your repo from the terminal.**
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ✨ Features
|
|
52
|
+
|
|
53
|
+
- 🤖 **Multi-provider AI** — Gemini (default, free), Groq (fastest), Ollama (100% local), OpenAI
|
|
54
|
+
- 💬 **Natural language commands** — `gitwise do "stash, switch to main, pull"`
|
|
55
|
+
- 🔍 **AI code review** — bugs, security issues, quality flags before you push
|
|
56
|
+
- 🚑 **Emergency recovery** — `gitwise whoops` diagnoses and fixes git disasters
|
|
57
|
+
- 📋 **PR generation** — full GitHub PR description, auto-copied to clipboard
|
|
58
|
+
- 🎨 **Beautiful Rich UI** — tables, panels, spinners, color-coded output
|
|
59
|
+
- 🔐 **Secure config** — keys stored in `~/.gitdude/config.json`, never in `.env`
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🚀 Quick Start
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Install
|
|
67
|
+
pip install gitdude
|
|
68
|
+
|
|
69
|
+
# 2. Configure (takes 60 seconds)
|
|
70
|
+
gitwise config
|
|
71
|
+
|
|
72
|
+
# 3. Generate an AI commit message and push
|
|
73
|
+
gitwise push
|
|
74
|
+
|
|
75
|
+
# 4. Review your code before merging
|
|
76
|
+
gitwise review
|
|
77
|
+
|
|
78
|
+
# 5. Something went wrong? Ask for help
|
|
79
|
+
gitwise whoops
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 📦 Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install gitdude
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Python 3.10+ required.**
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 🔑 Provider Setup
|
|
95
|
+
|
|
96
|
+
### Google Gemini (Default — Free)
|
|
97
|
+
|
|
98
|
+
1. Get an API key from [Google AI Studio](https://aistudio.google.com/app/apikey)
|
|
99
|
+
2. Run `gitwise config` → choose `gemini` → paste key
|
|
100
|
+
|
|
101
|
+
### Groq (Fastest Free Tier)
|
|
102
|
+
|
|
103
|
+
1. Get a key at [console.groq.com/keys](https://console.groq.com/keys)
|
|
104
|
+
2. Run `gitwise config` → choose `groq` → paste key
|
|
105
|
+
|
|
106
|
+
### Ollama (100% Local/Offline)
|
|
107
|
+
|
|
108
|
+
1. Install Ollama: [ollama.ai](https://ollama.ai)
|
|
109
|
+
2. Pull a model: `ollama pull llama3`
|
|
110
|
+
3. Run `gitwise config` → choose `ollama` (no key needed)
|
|
111
|
+
|
|
112
|
+
### OpenAI
|
|
113
|
+
|
|
114
|
+
1. Get a key at [platform.openai.com](https://platform.openai.com/api-keys)
|
|
115
|
+
2. Run `gitwise config` → choose `openai` → paste key
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 📖 Command Reference
|
|
120
|
+
|
|
121
|
+
### `gitwise push`
|
|
122
|
+
|
|
123
|
+
AI-generates a commit message for your changes, lets you confirm/edit, then commits and pushes.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
gitwise push # Stage all → AI commit → push
|
|
127
|
+
gitwise push --no-confirm # Skip confirmation
|
|
128
|
+
gitwise push --dry-run # Preview only, don't execute
|
|
129
|
+
gitwise push --style freeform # Use freeform (not conventional) commit style
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Commit types (conventional):** `feat`, `fix`, `chore`, `docs`, `refactor`, `style`, `test`, `perf`, `ci`
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### `gitwise sync`
|
|
137
|
+
|
|
138
|
+
Fetch + rebase. If there are merge conflicts, AI explains what's conflicting and gives exact resolution steps.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
gitwise sync
|
|
142
|
+
gitwise sync --dry-run
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### `gitwise back`
|
|
148
|
+
|
|
149
|
+
Shows last 30 commits in a table. Pick one to go back to, choose a mode.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
gitwise back
|
|
153
|
+
gitwise back --dry-run
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Modes:** `soft` (keep changes staged), `hard` (discard changes), `checkout` (detached HEAD), `branch` (new branch from that commit)
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `gitwise undo "<description>"`
|
|
161
|
+
|
|
162
|
+
Describe what went wrong — AI reads your log and reflog to determine the safest recovery.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
gitwise undo "I accidentally committed my .env file"
|
|
166
|
+
gitwise undo "I deleted the wrong branch"
|
|
167
|
+
gitwise undo "I made a mess of the last 3 commits" --dry-run
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### `gitwise do "<natural language>"`
|
|
173
|
+
|
|
174
|
+
Convert plain English into a sequence of git commands, preview, then execute.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
gitwise do "stash my changes, switch to main, pull latest"
|
|
178
|
+
gitwise do "create a new branch called feature/auth and push it"
|
|
179
|
+
gitwise do "squash my last 3 commits" --dry-run
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If any command fails, AI reads the error and suggests a fix.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### `gitwise review`
|
|
187
|
+
|
|
188
|
+
Diffs your current branch vs main/master, then AI reviews for bugs, security issues, and quality.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
gitwise review
|
|
192
|
+
gitwise review --base develop # Compare against a different branch
|
|
193
|
+
gitwise review --dry-run # Show diff without AI review
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Review sections:** Summary · ⚠️ Potential Bugs · 🔒 Security Issues · 📏 Code Quality · 🔍 Things to Check · ✅ Overall Assessment
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### `gitwise pr`
|
|
201
|
+
|
|
202
|
+
Generates a complete PR title + description + bullet list + testing notes. Auto-copies to clipboard.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
gitwise pr
|
|
206
|
+
gitwise pr --base develop # Compare against develop
|
|
207
|
+
gitwise pr --no-copy # Don't copy to clipboard
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `gitwise explain`
|
|
213
|
+
|
|
214
|
+
Reads your last 5 reflog entries and explains in plain English what happened.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
gitwise explain
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### `gitwise whoops`
|
|
223
|
+
|
|
224
|
+
Emergency recovery. Feeds git status + log + reflog to AI. Diagnoses what went wrong and gives step-by-step recovery.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
gitwise whoops
|
|
228
|
+
gitwise whoops --dry-run # Diagnose only, don't execute
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### `gitwise config`
|
|
234
|
+
|
|
235
|
+
Interactive configuration wizard.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
gitwise config # Run setup
|
|
239
|
+
gitwise config --show # Print current config (keys masked)
|
|
240
|
+
gitwise config --reset # Wipe config and redo setup
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Stored settings:**
|
|
244
|
+
- AI provider (`gemini` / `groq` / `ollama` / `openai`)
|
|
245
|
+
- API key per provider
|
|
246
|
+
- Model name per provider
|
|
247
|
+
- Default branch (`main` or `master`)
|
|
248
|
+
- Commit style (`conventional` or `freeform`)
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 🏗️ Architecture
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
gitwise/
|
|
256
|
+
├── main.py # Typer app — all command definitions
|
|
257
|
+
├── ai.py # Unified AI provider wrapper (ask_ai)
|
|
258
|
+
├── git_ops.py # GitPython operations (diff, log, push, reset…)
|
|
259
|
+
├── config.py # Config at ~/.gitdude/config.json
|
|
260
|
+
└── utils.py # Rich panels, tables, prompts, helpers
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**AI Providers (`ai.py`):**
|
|
264
|
+
|
|
265
|
+
| Provider | SDK | Default Model | Speed | Cost |
|
|
266
|
+
|---|---|---|---|---|
|
|
267
|
+
| `gemini` | `google-generativeai` | `gemini-2.0-flash` | Fast | Free tier |
|
|
268
|
+
| `groq` | `groq` | `llama-3.3-70b-versatile` | Fastest | Free tier |
|
|
269
|
+
| `ollama` | `ollama` | `llama3` | Local | Free (local) |
|
|
270
|
+
| `openai` | `openai` | `gpt-4o-mini` | Fast | Pay per use |
|
|
271
|
+
|
|
272
|
+
All providers go through a single interface: `ask_ai(prompt) -> str` with spinner feedback and unified error handling.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 🛡️ Safety Features
|
|
277
|
+
|
|
278
|
+
- ✅ **All destructive operations** (hard reset, force push, etc.) require explicit confirmation
|
|
279
|
+
- ✅ **`--dry-run` flag** available on all mutating commands
|
|
280
|
+
- ✅ **Color-coded risk levels** — green (safe), yellow (caution), red (destructive)
|
|
281
|
+
- ✅ **No tracebacks** — all exceptions caught and shown as friendly Rich error panels
|
|
282
|
+
- ✅ **API keys** stored privately in `~/.gitdude/config.json`, never in project files
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 🤝 Contributing
|
|
287
|
+
|
|
288
|
+
1. Fork the repo
|
|
289
|
+
2. Create a feature branch: `git checkout -b feat/amazing-feature`
|
|
290
|
+
3. Make your changes
|
|
291
|
+
4. Run linting: `ruff check .`
|
|
292
|
+
5. Open a PR — or just use `gitwise pr` to generate your PR description! 😄
|
|
293
|
+
|
|
294
|
+
**Dev setup:**
|
|
295
|
+
```bash
|
|
296
|
+
git clone https://github.com/gitwise/gitwise
|
|
297
|
+
cd gitwise
|
|
298
|
+
pip install -e ".[dev]"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 📄 License
|
|
304
|
+
|
|
305
|
+
MIT © GitWise Contributors
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
gitwise/__init__.py,sha256=mXFDA4plrZAZBHC1UQTMQRovGMNs4fHgCVNP-nwoSGM,132
|
|
2
|
+
gitwise/ai.py,sha256=fIcRAzlFxy4IIT6oiBOuRNie7B0STD5iPil00tJ_bKM,4757
|
|
3
|
+
gitwise/config.py,sha256=MfPlqAJ2pAwF4fNHl7imFYY-wXDj4Bxec-z_4MZKo1Q,2902
|
|
4
|
+
gitwise/git_ops.py,sha256=BIvF7l-OU8MwxKP7DqYA2DzTsDpQypQ5SLujwKR6sW0,6759
|
|
5
|
+
gitwise/main.py,sha256=xylYXLW_xP6Ui-YIM1_7d2TzwJFvizVGuhaPeRJw52s,31504
|
|
6
|
+
gitwise/utils.py,sha256=VV4RJ9Uqbf-XWbtwSIk_DGLO_0NMhQ80X9pFLB-Blgc,5154
|
|
7
|
+
gitdude-1.0.0.dist-info/METADATA,sha256=jL8pf71h4IZMIaXEQbTcYrygZAaPtBhPpBKf7D0I0Os,8874
|
|
8
|
+
gitdude-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
gitdude-1.0.0.dist-info/entry_points.txt,sha256=AWdMzCVsC0k4pMbdRnbB5aYpNsrkoiwi3rLuYis36JM,46
|
|
10
|
+
gitdude-1.0.0.dist-info/RECORD,,
|
gitwise/__init__.py
ADDED
gitwise/ai.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ai.py — Unified AI provider wrapper.
|
|
3
|
+
|
|
4
|
+
Single public function: ask_ai(prompt: str) -> str
|
|
5
|
+
Supports: gemini, groq, ollama, openai
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.live import Live
|
|
15
|
+
from rich.spinner import Spinner
|
|
16
|
+
|
|
17
|
+
from gitwise.config import (
|
|
18
|
+
get_config,
|
|
19
|
+
get_current_provider,
|
|
20
|
+
get_model_for_provider,
|
|
21
|
+
get_provider_api_key,
|
|
22
|
+
is_configured,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _spinner_context(message: str = "🤖 Thinking..."):
|
|
29
|
+
"""Return a Rich Live context showing a spinner."""
|
|
30
|
+
return Live(Spinner("dots", text=f"[bold magenta]{message}[/bold magenta]"), console=console, transient=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Provider implementations
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def _ask_gemini(prompt: str, model: str, api_key: str) -> str:
|
|
38
|
+
try:
|
|
39
|
+
import google.generativeai as genai # type: ignore
|
|
40
|
+
except ImportError:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"google-generativeai is not installed. Run: pip install google-generativeai"
|
|
43
|
+
)
|
|
44
|
+
genai.configure(api_key=api_key)
|
|
45
|
+
client = genai.GenerativeModel(model)
|
|
46
|
+
response = client.generate_content(prompt)
|
|
47
|
+
return response.text.strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ask_groq(prompt: str, model: str, api_key: str) -> str:
|
|
51
|
+
try:
|
|
52
|
+
from groq import Groq # type: ignore
|
|
53
|
+
except ImportError:
|
|
54
|
+
raise RuntimeError("groq is not installed. Run: pip install groq")
|
|
55
|
+
client = Groq(api_key=api_key)
|
|
56
|
+
chat_completion = client.chat.completions.create(
|
|
57
|
+
messages=[{"role": "user", "content": prompt}],
|
|
58
|
+
model=model,
|
|
59
|
+
)
|
|
60
|
+
return chat_completion.choices[0].message.content.strip()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ask_ollama(prompt: str, model: str, **_kwargs) -> str:
|
|
64
|
+
try:
|
|
65
|
+
import ollama # type: ignore
|
|
66
|
+
except ImportError:
|
|
67
|
+
raise RuntimeError("ollama is not installed. Run: pip install ollama")
|
|
68
|
+
response = ollama.chat(
|
|
69
|
+
model=model,
|
|
70
|
+
messages=[{"role": "user", "content": prompt}],
|
|
71
|
+
)
|
|
72
|
+
return response["message"]["content"].strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _ask_openai(prompt: str, model: str, api_key: str) -> str:
|
|
76
|
+
try:
|
|
77
|
+
from openai import OpenAI # type: ignore
|
|
78
|
+
except ImportError:
|
|
79
|
+
raise RuntimeError("openai is not installed. Run: pip install openai")
|
|
80
|
+
client = OpenAI(api_key=api_key)
|
|
81
|
+
completion = client.chat.completions.create(
|
|
82
|
+
model=model,
|
|
83
|
+
messages=[{"role": "user", "content": prompt}],
|
|
84
|
+
)
|
|
85
|
+
return completion.choices[0].message.content.strip()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Public interface
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def ask_ai(prompt: str, spinner_msg: str = "🤖 Thinking...") -> str:
|
|
93
|
+
"""
|
|
94
|
+
Send a prompt to the configured AI provider and return the response.
|
|
95
|
+
Shows a Rich spinner while waiting.
|
|
96
|
+
Raises SystemExit on unrecoverable errors.
|
|
97
|
+
"""
|
|
98
|
+
from gitwise.utils import error_panel # late import to avoid circular
|
|
99
|
+
|
|
100
|
+
if not is_configured():
|
|
101
|
+
error_panel(
|
|
102
|
+
"GitWise is not configured yet.\nRun [bold cyan]gitwise config[/bold cyan] to get started.",
|
|
103
|
+
title="❌ Not Configured",
|
|
104
|
+
)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
cfg = get_config()
|
|
108
|
+
provider = get_current_provider()
|
|
109
|
+
model = get_model_for_provider(provider)
|
|
110
|
+
api_key = get_provider_api_key(provider)
|
|
111
|
+
|
|
112
|
+
if provider != "ollama" and not api_key:
|
|
113
|
+
error_panel(
|
|
114
|
+
f"No API key found for provider [bold]{provider}[/bold].\n"
|
|
115
|
+
f"Run [bold cyan]gitwise config[/bold cyan] to set your key.",
|
|
116
|
+
title="❌ Missing API Key",
|
|
117
|
+
)
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
provider_fn = {
|
|
121
|
+
"gemini": _ask_gemini,
|
|
122
|
+
"groq": _ask_groq,
|
|
123
|
+
"ollama": _ask_ollama,
|
|
124
|
+
"openai": _ask_openai,
|
|
125
|
+
}.get(provider)
|
|
126
|
+
|
|
127
|
+
if provider_fn is None:
|
|
128
|
+
error_panel(
|
|
129
|
+
f"Unknown provider: [bold]{provider}[/bold]\n"
|
|
130
|
+
f"Run [bold cyan]gitwise config[/bold cyan] to choose a valid provider.",
|
|
131
|
+
title="❌ Invalid Provider",
|
|
132
|
+
)
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
with _spinner_context(spinner_msg):
|
|
137
|
+
result = provider_fn(prompt=prompt, model=model, api_key=api_key)
|
|
138
|
+
return result
|
|
139
|
+
except RuntimeError as exc:
|
|
140
|
+
error_panel(str(exc), title="❌ AI Provider Error")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
except Exception as exc: # noqa: BLE001
|
|
143
|
+
error_panel(
|
|
144
|
+
f"An error occurred while calling [bold]{provider}[/bold]:\n{exc}\n\n"
|
|
145
|
+
"Check your API key, network connection, and model name.",
|
|
146
|
+
title="❌ AI Call Failed",
|
|
147
|
+
)
|
|
148
|
+
sys.exit(1)
|
gitwise/config.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
config.py — GitWise configuration management.
|
|
3
|
+
Stores config in ~/.gitwise/config.json.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
CONFIG_DIR = Path.home() / ".gitwise"
|
|
14
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
15
|
+
|
|
16
|
+
PROVIDERS = ["gemini", "groq", "ollama", "openai"]
|
|
17
|
+
|
|
18
|
+
DEFAULTS: dict[str, Any] = {
|
|
19
|
+
"provider": "gemini",
|
|
20
|
+
"model": {
|
|
21
|
+
"gemini": "gemini-2.0-flash",
|
|
22
|
+
"groq": "llama-3.3-70b-versatile",
|
|
23
|
+
"ollama": "llama3",
|
|
24
|
+
"openai": "gpt-4o-mini",
|
|
25
|
+
},
|
|
26
|
+
"api_key": {
|
|
27
|
+
"gemini": "",
|
|
28
|
+
"groq": "",
|
|
29
|
+
"ollama": "",
|
|
30
|
+
"openai": "",
|
|
31
|
+
},
|
|
32
|
+
"default_branch": "main",
|
|
33
|
+
"commit_style": "conventional",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_config() -> dict[str, Any]:
|
|
38
|
+
"""Load config from disk, returning defaults if missing."""
|
|
39
|
+
if not CONFIG_FILE.exists():
|
|
40
|
+
return {}
|
|
41
|
+
try:
|
|
42
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
43
|
+
return json.load(f)
|
|
44
|
+
except (json.JSONDecodeError, OSError):
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_config(cfg: dict[str, Any]) -> None:
|
|
49
|
+
"""Persist config to disk."""
|
|
50
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
52
|
+
json.dump(cfg, f, indent=2)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_config() -> dict[str, Any]:
|
|
56
|
+
"""Return merged config with defaults."""
|
|
57
|
+
cfg = load_config()
|
|
58
|
+
# Deep merge with defaults
|
|
59
|
+
merged = {
|
|
60
|
+
"provider": cfg.get("provider", DEFAULTS["provider"]),
|
|
61
|
+
"model": {**DEFAULTS["model"], **cfg.get("model", {})},
|
|
62
|
+
"api_key": {**DEFAULTS["api_key"], **cfg.get("api_key", {})},
|
|
63
|
+
"default_branch": cfg.get("default_branch", DEFAULTS["default_branch"]),
|
|
64
|
+
"commit_style": cfg.get("commit_style", DEFAULTS["commit_style"]),
|
|
65
|
+
}
|
|
66
|
+
return merged
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_configured() -> bool:
|
|
70
|
+
"""Return True if a config file exists with at least a provider set."""
|
|
71
|
+
return CONFIG_FILE.exists()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_provider_api_key(provider: str) -> str:
|
|
75
|
+
"""Get API key for the given provider from config."""
|
|
76
|
+
cfg = get_config()
|
|
77
|
+
# Also allow environment variable overrides
|
|
78
|
+
env_map = {
|
|
79
|
+
"gemini": "GEMINI_API_KEY",
|
|
80
|
+
"groq": "GROQ_API_KEY",
|
|
81
|
+
"openai": "OPENAI_API_KEY",
|
|
82
|
+
"ollama": "",
|
|
83
|
+
}
|
|
84
|
+
env_key = env_map.get(provider, "")
|
|
85
|
+
if env_key:
|
|
86
|
+
env_val = os.environ.get(env_key, "")
|
|
87
|
+
if env_val:
|
|
88
|
+
return env_val
|
|
89
|
+
return cfg.get("api_key", {}).get(provider, "")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_current_provider() -> str:
|
|
93
|
+
return get_config().get("provider", "gemini")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_model_for_provider(provider: str) -> str:
|
|
97
|
+
return get_config().get("model", {}).get(provider, DEFAULTS["model"].get(provider, ""))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def mask_key(key: str) -> str:
|
|
101
|
+
"""Mask an API key for display."""
|
|
102
|
+
if not key:
|
|
103
|
+
return "<not set>"
|
|
104
|
+
if len(key) <= 8:
|
|
105
|
+
return "****"
|
|
106
|
+
return key[:4] + "****" + key[-4:]
|