devcommit 0.1.5.6__tar.gz → 0.1.5.7__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.
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/PKG-INFO +18 -2
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/README.md +17 -1
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/app/ai_providers.py +47 -12
- devcommit-0.1.5.7/devcommit/app/model_catalog.py +246 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/create_config.py +4 -1
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/main.py +45 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/utils/git.py +44 -18
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/utils/parser.py +24 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/pyproject.toml +2 -2
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/COPYING +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/__init__.py +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/__version__.py +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/app/__init__.py +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/app/changelog.py +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/app/gemini_ai.py +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/app/prompt.py +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/utils/__init__.py +0 -0
- {devcommit-0.1.5.6 → devcommit-0.1.5.7}/devcommit/utils/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devcommit
|
|
3
|
-
Version: 0.1.5.
|
|
3
|
+
Version: 0.1.5.7
|
|
4
4
|
Summary: AI-powered git commit message generator
|
|
5
5
|
License: GNU GENERAL PUBLIC LICENSE
|
|
6
6
|
Version 3, 29 June 2007
|
|
@@ -1184,6 +1184,8 @@ devcommit --stageAll --changelog
|
|
|
1184
1184
|
- `--files` or `-f`: Stage and commit specific files or folders (can specify multiple)
|
|
1185
1185
|
- `--push` or `-p`: Push commits to remote after committing
|
|
1186
1186
|
- `--changelog` or `-c`: Generate changelog file from changes
|
|
1187
|
+
- `--models`: Fetch and list models from the configured `AI_PROVIDER`
|
|
1188
|
+
- `--free` / `--paid`: With `--models`, limit OpenRouter results to free or paid models
|
|
1187
1189
|
|
|
1188
1190
|
### Examples
|
|
1189
1191
|
|
|
@@ -1262,6 +1264,19 @@ export OPENROUTER_MODEL='meta-llama/llama-3.3-70b-instruct:free'
|
|
|
1262
1264
|
devcommit
|
|
1263
1265
|
```
|
|
1264
1266
|
|
|
1267
|
+
To fetch the live model catalogue for your configured provider without opening a browser:
|
|
1268
|
+
|
|
1269
|
+
```bash
|
|
1270
|
+
AI_PROVIDER=openrouter devcommit --models
|
|
1271
|
+
|
|
1272
|
+
# Only free OpenRouter models
|
|
1273
|
+
AI_PROVIDER=openrouter devcommit --models --free
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
For OpenRouter, the table identifies free and paid models and shows their live token prices. For other providers, it lists the models accessible with that provider's configured API key; Ollama lists models installed locally. Copy an ID into the matching provider model setting, such as `OPENROUTER_MODEL`.
|
|
1277
|
+
|
|
1278
|
+
Set `REASONING_EFFORT=none` to disable reasoning where the selected model supports it and favor faster commit messages. The default is `auto`; `low`, `medium`, `high`, `xhigh`, and `max` are also accepted where supported.
|
|
1279
|
+
|
|
1265
1280
|
**Popular free models on OpenRouter (add `:free` suffix):**
|
|
1266
1281
|
|
|
1267
1282
|
**Recommended Models:**
|
|
@@ -1363,7 +1378,8 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
|
|
|
1363
1378
|
| `MAX_NO` | Number of commit message suggestions | `1` | Any positive integer |
|
|
1364
1379
|
| `COMMIT_TYPE` | Style of commit messages | `general` | `general`, `conventional`, etc. |
|
|
1365
1380
|
| `COMMIT_MODE` | Default commit strategy | `auto` | `auto`, `directory`, `global`, `related` |
|
|
1366
|
-
| `
|
|
1381
|
+
| `REASONING_EFFORT` | Reasoning effort for supported models | `auto` | `auto`, `none`, `low`, `medium`, `high`, `xhigh`, `max` |
|
|
1382
|
+
| `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated patterns or absolute paths within one repo |
|
|
1367
1383
|
| `MAX_TOKENS` | Maximum tokens for AI response | `8192` | Any positive integer |
|
|
1368
1384
|
| `CHANGELOG_DIR` | Directory for changelog files | `changelogs` | Any directory path |
|
|
1369
1385
|
| `CHANGELOG_MODE` | Changelog file strategy | `timestamped` | `timestamped`, `branch` |
|
|
@@ -479,6 +479,8 @@ devcommit --stageAll --changelog
|
|
|
479
479
|
- `--files` or `-f`: Stage and commit specific files or folders (can specify multiple)
|
|
480
480
|
- `--push` or `-p`: Push commits to remote after committing
|
|
481
481
|
- `--changelog` or `-c`: Generate changelog file from changes
|
|
482
|
+
- `--models`: Fetch and list models from the configured `AI_PROVIDER`
|
|
483
|
+
- `--free` / `--paid`: With `--models`, limit OpenRouter results to free or paid models
|
|
482
484
|
|
|
483
485
|
### Examples
|
|
484
486
|
|
|
@@ -557,6 +559,19 @@ export OPENROUTER_MODEL='meta-llama/llama-3.3-70b-instruct:free'
|
|
|
557
559
|
devcommit
|
|
558
560
|
```
|
|
559
561
|
|
|
562
|
+
To fetch the live model catalogue for your configured provider without opening a browser:
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
AI_PROVIDER=openrouter devcommit --models
|
|
566
|
+
|
|
567
|
+
# Only free OpenRouter models
|
|
568
|
+
AI_PROVIDER=openrouter devcommit --models --free
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
For OpenRouter, the table identifies free and paid models and shows their live token prices. For other providers, it lists the models accessible with that provider's configured API key; Ollama lists models installed locally. Copy an ID into the matching provider model setting, such as `OPENROUTER_MODEL`.
|
|
572
|
+
|
|
573
|
+
Set `REASONING_EFFORT=none` to disable reasoning where the selected model supports it and favor faster commit messages. The default is `auto`; `low`, `medium`, `high`, `xhigh`, and `max` are also accepted where supported.
|
|
574
|
+
|
|
560
575
|
**Popular free models on OpenRouter (add `:free` suffix):**
|
|
561
576
|
|
|
562
577
|
**Recommended Models:**
|
|
@@ -658,7 +673,8 @@ All configuration can be set via **environment variables** or **`.dcommit` file*
|
|
|
658
673
|
| `MAX_NO` | Number of commit message suggestions | `1` | Any positive integer |
|
|
659
674
|
| `COMMIT_TYPE` | Style of commit messages | `general` | `general`, `conventional`, etc. |
|
|
660
675
|
| `COMMIT_MODE` | Default commit strategy | `auto` | `auto`, `directory`, `global`, `related` |
|
|
661
|
-
| `
|
|
676
|
+
| `REASONING_EFFORT` | Reasoning effort for supported models | `auto` | `auto`, `none`, `low`, `medium`, `high`, `xhigh`, `max` |
|
|
677
|
+
| `EXCLUDE_FILES` | Files to exclude from diff | `package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock` | Comma-separated patterns or absolute paths within one repo |
|
|
662
678
|
| `MAX_TOKENS` | Maximum tokens for AI response | `8192` | Any positive integer |
|
|
663
679
|
| `CHANGELOG_DIR` | Directory for changelog files | `changelogs` | Any directory path |
|
|
664
680
|
| `CHANGELOG_MODE` | Changelog file strategy | `timestamped` | `timestamped`, `branch` |
|
|
@@ -34,6 +34,19 @@ except ImportError:
|
|
|
34
34
|
sys.stderr = _stderr
|
|
35
35
|
_devnull.close()
|
|
36
36
|
|
|
37
|
+
REASONING_EFFORTS = {"auto", "none", "low", "medium", "high", "xhigh", "max"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize_reasoning_effort(value: str) -> str:
|
|
41
|
+
effort = value.lower()
|
|
42
|
+
if effort not in REASONING_EFFORTS:
|
|
43
|
+
raise ValueError("REASONING_EFFORT must be auto, none, low, medium, high, xhigh, or max")
|
|
44
|
+
return effort
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def reasoning_extra_body(effort: str) -> dict:
|
|
48
|
+
return {} if effort == "auto" else {"reasoning_effort": effort}
|
|
49
|
+
|
|
37
50
|
|
|
38
51
|
class AIProvider(ABC):
|
|
39
52
|
"""Base class for AI providers"""
|
|
@@ -106,11 +119,12 @@ class GeminiProvider(AIProvider):
|
|
|
106
119
|
class OpenAIProvider(AIProvider):
|
|
107
120
|
"""OpenAI GPT provider"""
|
|
108
121
|
|
|
109
|
-
def __init__(self, api_key: str, model: str = "gpt-4o-mini"):
|
|
122
|
+
def __init__(self, api_key: str, model: str = "gpt-4o-mini", reasoning_effort: str = "auto"):
|
|
110
123
|
if not openai:
|
|
111
124
|
raise ImportError("openai not installed. Run: pip install openai")
|
|
112
125
|
self.client = openai.OpenAI(api_key=api_key)
|
|
113
126
|
self.model = model
|
|
127
|
+
self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
|
|
114
128
|
|
|
115
129
|
def generate_commit_message(
|
|
116
130
|
self, diff: str, prompt: str, max_tokens: int
|
|
@@ -123,6 +137,7 @@ class OpenAIProvider(AIProvider):
|
|
|
123
137
|
],
|
|
124
138
|
max_tokens=max_tokens,
|
|
125
139
|
temperature=0.7,
|
|
140
|
+
extra_body=reasoning_extra_body(self.reasoning_effort),
|
|
126
141
|
)
|
|
127
142
|
if not response or not response.choices:
|
|
128
143
|
raise ValueError("OpenAI returned empty response (no choices).")
|
|
@@ -132,13 +147,14 @@ class OpenAIProvider(AIProvider):
|
|
|
132
147
|
class GroqProvider(AIProvider):
|
|
133
148
|
"""Groq AI provider (OpenAI-compatible)"""
|
|
134
149
|
|
|
135
|
-
def __init__(self, api_key: str, model: str = "llama-3.3-70b-versatile"):
|
|
150
|
+
def __init__(self, api_key: str, model: str = "llama-3.3-70b-versatile", reasoning_effort: str = "auto"):
|
|
136
151
|
if not openai:
|
|
137
152
|
raise ImportError("openai not installed. Run: pip install openai")
|
|
138
153
|
self.client = openai.OpenAI(
|
|
139
154
|
api_key=api_key, base_url="https://api.groq.com/openai/v1"
|
|
140
155
|
)
|
|
141
156
|
self.model = model
|
|
157
|
+
self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
|
|
142
158
|
|
|
143
159
|
def generate_commit_message(
|
|
144
160
|
self, diff: str, prompt: str, max_tokens: int
|
|
@@ -151,6 +167,7 @@ class GroqProvider(AIProvider):
|
|
|
151
167
|
],
|
|
152
168
|
max_tokens=max_tokens,
|
|
153
169
|
temperature=0.7,
|
|
170
|
+
extra_body=reasoning_extra_body(self.reasoning_effort),
|
|
154
171
|
)
|
|
155
172
|
if not response or not response.choices:
|
|
156
173
|
raise ValueError("Groq returned empty response (no choices).")
|
|
@@ -164,6 +181,7 @@ class OpenRouterProvider(AIProvider):
|
|
|
164
181
|
self,
|
|
165
182
|
api_key: str,
|
|
166
183
|
model: str = "meta-llama/llama-3.3-70b-instruct:free",
|
|
184
|
+
reasoning_effort: str = "auto",
|
|
167
185
|
):
|
|
168
186
|
if not openai:
|
|
169
187
|
raise ImportError("openai not installed. Run: pip install openai")
|
|
@@ -176,6 +194,7 @@ class OpenRouterProvider(AIProvider):
|
|
|
176
194
|
},
|
|
177
195
|
)
|
|
178
196
|
self.model = model
|
|
197
|
+
self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
|
|
179
198
|
|
|
180
199
|
def generate_commit_message(
|
|
181
200
|
self, diff: str, prompt: str, max_tokens: int
|
|
@@ -188,7 +207,7 @@ class OpenRouterProvider(AIProvider):
|
|
|
188
207
|
],
|
|
189
208
|
max_tokens=max_tokens,
|
|
190
209
|
temperature=0.7,
|
|
191
|
-
extra_body={"transforms": ["middle-out"]},
|
|
210
|
+
extra_body={"transforms": ["middle-out"], **reasoning_extra_body(self.reasoning_effort)},
|
|
192
211
|
extra_headers={
|
|
193
212
|
"HTTP-Referer": "https://github.com/hordunlarmy/DevCommit",
|
|
194
213
|
"X-Title": "DevCommit",
|
|
@@ -207,23 +226,30 @@ class OpenRouterProvider(AIProvider):
|
|
|
207
226
|
class AnthropicProvider(AIProvider):
|
|
208
227
|
"""Anthropic Claude provider"""
|
|
209
228
|
|
|
210
|
-
def __init__(self, api_key: str, model: str = "claude-3-haiku-20240307"):
|
|
229
|
+
def __init__(self, api_key: str, model: str = "claude-3-haiku-20240307", reasoning_effort: str = "auto"):
|
|
211
230
|
if not anthropic:
|
|
212
231
|
raise ImportError(
|
|
213
232
|
"anthropic not installed. Run: pip install anthropic"
|
|
214
233
|
)
|
|
215
234
|
self.client = anthropic.Anthropic(api_key=api_key)
|
|
216
235
|
self.model = model
|
|
236
|
+
self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
|
|
217
237
|
|
|
218
238
|
def generate_commit_message(
|
|
219
239
|
self, diff: str, prompt: str, max_tokens: int
|
|
220
240
|
) -> str:
|
|
221
|
-
|
|
241
|
+
request = dict(
|
|
222
242
|
model=self.model,
|
|
223
243
|
max_tokens=max_tokens,
|
|
224
244
|
system=prompt,
|
|
225
245
|
messages=[{"role": "user", "content": diff}],
|
|
226
246
|
)
|
|
247
|
+
if self.reasoning_effort == "none":
|
|
248
|
+
request["thinking"] = {"type": "disabled"}
|
|
249
|
+
elif self.reasoning_effort != "auto":
|
|
250
|
+
request["thinking"] = {"type": "adaptive"}
|
|
251
|
+
request["output_config"] = {"effort": self.reasoning_effort}
|
|
252
|
+
message = self.client.messages.create(**request)
|
|
227
253
|
return message.content[0].text.strip()
|
|
228
254
|
|
|
229
255
|
|
|
@@ -231,7 +257,7 @@ class OllamaProvider(AIProvider):
|
|
|
231
257
|
"""Ollama local model provider"""
|
|
232
258
|
|
|
233
259
|
def __init__(
|
|
234
|
-
self, base_url: str = "http://localhost:11434", model: str = "llama3"
|
|
260
|
+
self, base_url: str = "http://localhost:11434", model: str = "llama3", reasoning_effort: str = "auto"
|
|
235
261
|
):
|
|
236
262
|
if not requests:
|
|
237
263
|
raise ImportError(
|
|
@@ -239,6 +265,7 @@ class OllamaProvider(AIProvider):
|
|
|
239
265
|
)
|
|
240
266
|
self.base_url = base_url.rstrip("/")
|
|
241
267
|
self.model = model
|
|
268
|
+
self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
|
|
242
269
|
|
|
243
270
|
def generate_commit_message(
|
|
244
271
|
self, diff: str, prompt: str, max_tokens: int
|
|
@@ -250,6 +277,8 @@ class OllamaProvider(AIProvider):
|
|
|
250
277
|
"stream": False,
|
|
251
278
|
"options": {"temperature": 0.7, "num_predict": max_tokens},
|
|
252
279
|
}
|
|
280
|
+
if self.reasoning_effort != "auto":
|
|
281
|
+
data["think"] = False if self.reasoning_effort == "none" else self.reasoning_effort
|
|
253
282
|
|
|
254
283
|
response = requests.post(url, json=data, timeout=60)
|
|
255
284
|
response.raise_for_status()
|
|
@@ -267,6 +296,7 @@ class CustomProvider(AIProvider):
|
|
|
267
296
|
api_url: str,
|
|
268
297
|
api_key: Optional[str] = None,
|
|
269
298
|
model: str = "default",
|
|
299
|
+
reasoning_effort: str = "auto",
|
|
270
300
|
):
|
|
271
301
|
if not openai:
|
|
272
302
|
raise ImportError("openai not installed. Run: pip install openai")
|
|
@@ -282,6 +312,7 @@ class CustomProvider(AIProvider):
|
|
|
282
312
|
api_key=api_key or "dummy-key", base_url=base_url
|
|
283
313
|
)
|
|
284
314
|
self.model = model
|
|
315
|
+
self.reasoning_effort = normalize_reasoning_effort(reasoning_effort)
|
|
285
316
|
|
|
286
317
|
def generate_commit_message(
|
|
287
318
|
self, diff: str, prompt: str, max_tokens: int
|
|
@@ -294,6 +325,7 @@ class CustomProvider(AIProvider):
|
|
|
294
325
|
],
|
|
295
326
|
max_tokens=max_tokens,
|
|
296
327
|
temperature=0.7,
|
|
328
|
+
extra_body=reasoning_extra_body(self.reasoning_effort),
|
|
297
329
|
)
|
|
298
330
|
if not response or not response.choices:
|
|
299
331
|
raise ValueError("Custom API returned empty response (no choices).")
|
|
@@ -304,6 +336,7 @@ def get_ai_provider(config) -> AIProvider:
|
|
|
304
336
|
"""Factory function to get the appropriate AI provider based on config"""
|
|
305
337
|
|
|
306
338
|
provider_name = config("AI_PROVIDER", default="gemini").lower()
|
|
339
|
+
reasoning_effort = config("REASONING_EFFORT", default="auto")
|
|
307
340
|
|
|
308
341
|
if provider_name == "gemini":
|
|
309
342
|
api_key = config("GEMINI_API_KEY", default=None)
|
|
@@ -313,6 +346,8 @@ def get_ai_provider(config) -> AIProvider:
|
|
|
313
346
|
model = config("GEMINI_MODEL", default=None) or config(
|
|
314
347
|
"MODEL_NAME", default="gemini-2.0-flash-exp"
|
|
315
348
|
)
|
|
349
|
+
if normalize_reasoning_effort(reasoning_effort) != "auto":
|
|
350
|
+
raise ValueError("REASONING_EFFORT is unsupported by the legacy Gemini SDK; use auto")
|
|
316
351
|
return GeminiProvider(api_key, model)
|
|
317
352
|
|
|
318
353
|
elif provider_name == "openai":
|
|
@@ -322,7 +357,7 @@ def get_ai_provider(config) -> AIProvider:
|
|
|
322
357
|
model = config("OPENAI_MODEL", default=None) or config(
|
|
323
358
|
"MODEL_NAME", default="gpt-4o-mini"
|
|
324
359
|
)
|
|
325
|
-
return OpenAIProvider(api_key, model)
|
|
360
|
+
return OpenAIProvider(api_key, model, reasoning_effort)
|
|
326
361
|
|
|
327
362
|
elif provider_name == "groq":
|
|
328
363
|
api_key = config("GROQ_API_KEY", default=None)
|
|
@@ -331,7 +366,7 @@ def get_ai_provider(config) -> AIProvider:
|
|
|
331
366
|
model = config("GROQ_MODEL", default=None) or config(
|
|
332
367
|
"MODEL_NAME", default="llama-3.3-70b-versatile"
|
|
333
368
|
)
|
|
334
|
-
return GroqProvider(api_key, model)
|
|
369
|
+
return GroqProvider(api_key, model, reasoning_effort)
|
|
335
370
|
|
|
336
371
|
elif provider_name == "openrouter":
|
|
337
372
|
api_key = config("OPENROUTER_API_KEY", default=None)
|
|
@@ -340,7 +375,7 @@ def get_ai_provider(config) -> AIProvider:
|
|
|
340
375
|
model = config("OPENROUTER_MODEL", default=None) or config(
|
|
341
376
|
"MODEL_NAME", default="meta-llama/llama-3.3-70b-instruct:free"
|
|
342
377
|
)
|
|
343
|
-
return OpenRouterProvider(api_key, model)
|
|
378
|
+
return OpenRouterProvider(api_key, model, reasoning_effort)
|
|
344
379
|
|
|
345
380
|
elif provider_name == "anthropic":
|
|
346
381
|
api_key = config("ANTHROPIC_API_KEY", default=None)
|
|
@@ -349,14 +384,14 @@ def get_ai_provider(config) -> AIProvider:
|
|
|
349
384
|
model = config("ANTHROPIC_MODEL", default=None) or config(
|
|
350
385
|
"MODEL_NAME", default="claude-3-haiku-20240307"
|
|
351
386
|
)
|
|
352
|
-
return AnthropicProvider(api_key, model)
|
|
387
|
+
return AnthropicProvider(api_key, model, reasoning_effort)
|
|
353
388
|
|
|
354
389
|
elif provider_name == "ollama":
|
|
355
390
|
base_url = config("OLLAMA_BASE_URL", default="http://localhost:11434")
|
|
356
391
|
model = config("OLLAMA_MODEL", default=None) or config(
|
|
357
392
|
"MODEL_NAME", default="llama3"
|
|
358
393
|
)
|
|
359
|
-
return OllamaProvider(base_url, model)
|
|
394
|
+
return OllamaProvider(base_url, model, reasoning_effort)
|
|
360
395
|
|
|
361
396
|
elif provider_name == "custom":
|
|
362
397
|
api_url = config("CUSTOM_API_URL", default=None)
|
|
@@ -366,7 +401,7 @@ def get_ai_provider(config) -> AIProvider:
|
|
|
366
401
|
model = config("CUSTOM_MODEL", default=None) or config(
|
|
367
402
|
"MODEL_NAME", default="default"
|
|
368
403
|
)
|
|
369
|
-
return CustomProvider(api_url, api_key, model)
|
|
404
|
+
return CustomProvider(api_url, api_key, model, reasoning_effort)
|
|
370
405
|
|
|
371
406
|
else:
|
|
372
407
|
raise ValueError(
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Provider-aware model catalogue helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Callable, Dict, List, Optional
|
|
5
|
+
from urllib.error import HTTPError, URLError
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Model = Dict[str, object]
|
|
11
|
+
Config = Callable[..., Optional[str]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelCatalogError(Exception):
|
|
15
|
+
"""Raised when a provider's model catalogue cannot be read."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _request_json(
|
|
19
|
+
url: str, headers: Optional[Dict[str, str]] = None, timeout: int = 10
|
|
20
|
+
) -> Dict[str, object]:
|
|
21
|
+
request_headers = {"User-Agent": "DevCommit"}
|
|
22
|
+
if headers:
|
|
23
|
+
request_headers.update(headers)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with urlopen(Request(url, headers=request_headers), timeout=timeout) as response:
|
|
27
|
+
payload = json.load(response)
|
|
28
|
+
except (HTTPError, URLError, TimeoutError, json.JSONDecodeError) as error:
|
|
29
|
+
raise ModelCatalogError(f"Could not fetch models: {error}") from error
|
|
30
|
+
|
|
31
|
+
if not isinstance(payload, dict):
|
|
32
|
+
raise ModelCatalogError("The provider returned an unexpected models response.")
|
|
33
|
+
return payload
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _api_key(config: Config, key_name: str) -> str:
|
|
37
|
+
api_key = config(key_name, default=None)
|
|
38
|
+
if not api_key:
|
|
39
|
+
raise ModelCatalogError(f"{key_name} is not set.")
|
|
40
|
+
return api_key
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _model(
|
|
44
|
+
model_id: str,
|
|
45
|
+
name: Optional[str] = None,
|
|
46
|
+
context_length: Optional[object] = None,
|
|
47
|
+
availability: str = "Available",
|
|
48
|
+
price: str = "-",
|
|
49
|
+
) -> Model:
|
|
50
|
+
return {
|
|
51
|
+
"id": model_id,
|
|
52
|
+
"name": name or model_id,
|
|
53
|
+
"context_length": context_length,
|
|
54
|
+
"availability": availability,
|
|
55
|
+
"price": price,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _models_from_openai_response(payload: Dict[str, object]) -> List[Model]:
|
|
60
|
+
data = payload.get("data")
|
|
61
|
+
if not isinstance(data, list):
|
|
62
|
+
raise ModelCatalogError("The provider returned an unexpected models response.")
|
|
63
|
+
|
|
64
|
+
models = []
|
|
65
|
+
for item in data:
|
|
66
|
+
if isinstance(item, dict) and isinstance(item.get("id"), str):
|
|
67
|
+
models.append(_model(item["id"], item.get("id")))
|
|
68
|
+
return models
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _format_openrouter_price(pricing: object) -> str:
|
|
72
|
+
if not isinstance(pricing, dict):
|
|
73
|
+
return "-"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
prompt = float(pricing.get("prompt", "0")) * 1_000_000
|
|
77
|
+
completion = float(pricing.get("completion", "0")) * 1_000_000
|
|
78
|
+
except (TypeError, ValueError):
|
|
79
|
+
return "-"
|
|
80
|
+
if prompt < 0 or completion < 0:
|
|
81
|
+
return "Variable"
|
|
82
|
+
return f"${prompt:g} in / ${completion:g} out per M"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _fetch_openrouter_models(config: Config, timeout: int) -> List[Model]:
|
|
86
|
+
api_key = config("OPENROUTER_API_KEY", default=None)
|
|
87
|
+
headers = {"Authorization": f"Bearer {api_key}"} if api_key else None
|
|
88
|
+
payload = _request_json(
|
|
89
|
+
"https://openrouter.ai/api/v1/models", headers, timeout
|
|
90
|
+
)
|
|
91
|
+
data = payload.get("data")
|
|
92
|
+
if not isinstance(data, list):
|
|
93
|
+
raise ModelCatalogError("OpenRouter returned an unexpected models response.")
|
|
94
|
+
|
|
95
|
+
models = []
|
|
96
|
+
for item in data:
|
|
97
|
+
if not isinstance(item, dict) or not isinstance(item.get("id"), str):
|
|
98
|
+
continue
|
|
99
|
+
model_id = item["id"]
|
|
100
|
+
is_free = model_id.endswith(":free") or model_id == "openrouter/free"
|
|
101
|
+
models.append(
|
|
102
|
+
_model(
|
|
103
|
+
model_id,
|
|
104
|
+
item.get("name"),
|
|
105
|
+
item.get("context_length"),
|
|
106
|
+
"Free" if is_free else "Paid",
|
|
107
|
+
"Free" if is_free else _format_openrouter_price(item.get("pricing")),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
return models
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _fetch_gemini_models(config: Config, timeout: int) -> List[Model]:
|
|
114
|
+
headers = {"x-goog-api-key": _api_key(config, "GEMINI_API_KEY")}
|
|
115
|
+
page_token = None
|
|
116
|
+
models = []
|
|
117
|
+
|
|
118
|
+
while True:
|
|
119
|
+
params = {"pageSize": "1000"}
|
|
120
|
+
if page_token:
|
|
121
|
+
params["pageToken"] = page_token
|
|
122
|
+
payload = _request_json(
|
|
123
|
+
"https://generativelanguage.googleapis.com/v1beta/models?"
|
|
124
|
+
+ urlencode(params),
|
|
125
|
+
headers,
|
|
126
|
+
timeout,
|
|
127
|
+
)
|
|
128
|
+
data = payload.get("models")
|
|
129
|
+
if not isinstance(data, list):
|
|
130
|
+
raise ModelCatalogError("Gemini returned an unexpected models response.")
|
|
131
|
+
|
|
132
|
+
for item in data:
|
|
133
|
+
if not isinstance(item, dict) or "generateContent" not in item.get(
|
|
134
|
+
"supportedGenerationMethods", []
|
|
135
|
+
):
|
|
136
|
+
continue
|
|
137
|
+
resource_name = item.get("name")
|
|
138
|
+
if not isinstance(resource_name, str):
|
|
139
|
+
continue
|
|
140
|
+
models.append(
|
|
141
|
+
_model(
|
|
142
|
+
resource_name.removeprefix("models/"),
|
|
143
|
+
item.get("displayName"),
|
|
144
|
+
item.get("inputTokenLimit"),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
page_token = payload.get("nextPageToken")
|
|
149
|
+
if not isinstance(page_token, str) or not page_token:
|
|
150
|
+
return models
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _fetch_anthropic_models(config: Config, timeout: int) -> List[Model]:
|
|
154
|
+
headers = {
|
|
155
|
+
"x-api-key": _api_key(config, "ANTHROPIC_API_KEY"),
|
|
156
|
+
"anthropic-version": "2023-06-01",
|
|
157
|
+
}
|
|
158
|
+
after_id = None
|
|
159
|
+
models = []
|
|
160
|
+
|
|
161
|
+
while True:
|
|
162
|
+
params = {"limit": "1000"}
|
|
163
|
+
if after_id:
|
|
164
|
+
params["after_id"] = after_id
|
|
165
|
+
payload = _request_json(
|
|
166
|
+
"https://api.anthropic.com/v1/models?" + urlencode(params),
|
|
167
|
+
headers,
|
|
168
|
+
timeout,
|
|
169
|
+
)
|
|
170
|
+
data = payload.get("data")
|
|
171
|
+
if not isinstance(data, list):
|
|
172
|
+
raise ModelCatalogError("Anthropic returned an unexpected models response.")
|
|
173
|
+
|
|
174
|
+
for item in data:
|
|
175
|
+
if isinstance(item, dict) and isinstance(item.get("id"), str):
|
|
176
|
+
models.append(
|
|
177
|
+
_model(
|
|
178
|
+
item["id"], item.get("display_name"), item.get("max_input_tokens")
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not payload.get("has_more"):
|
|
183
|
+
return models
|
|
184
|
+
after_id = payload.get("last_id")
|
|
185
|
+
if not isinstance(after_id, str) or not after_id:
|
|
186
|
+
raise ModelCatalogError("Anthropic returned an invalid pagination cursor.")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _fetch_ollama_models(config: Config, timeout: int) -> List[Model]:
|
|
190
|
+
base_url = config("OLLAMA_BASE_URL", default="http://localhost:11434")
|
|
191
|
+
payload = _request_json(base_url.rstrip("/") + "/api/tags", timeout=timeout)
|
|
192
|
+
data = payload.get("models")
|
|
193
|
+
if not isinstance(data, list):
|
|
194
|
+
raise ModelCatalogError("Ollama returned an unexpected models response.")
|
|
195
|
+
|
|
196
|
+
models = []
|
|
197
|
+
for item in data:
|
|
198
|
+
if not isinstance(item, dict):
|
|
199
|
+
continue
|
|
200
|
+
model_id = item.get("model") or item.get("name")
|
|
201
|
+
if isinstance(model_id, str):
|
|
202
|
+
details = item.get("details")
|
|
203
|
+
size = details.get("parameter_size") if isinstance(details, dict) else None
|
|
204
|
+
models.append(_model(model_id, item.get("name"), size, "Installed"))
|
|
205
|
+
return models
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _fetch_custom_models(config: Config, timeout: int) -> List[Model]:
|
|
209
|
+
base_url = config("CUSTOM_API_URL", default=None)
|
|
210
|
+
if not base_url:
|
|
211
|
+
raise ModelCatalogError("CUSTOM_API_URL is not set.")
|
|
212
|
+
api_key = config("CUSTOM_API_KEY", default=None)
|
|
213
|
+
headers = {"Authorization": f"Bearer {api_key}"} if api_key else None
|
|
214
|
+
return _models_from_openai_response(
|
|
215
|
+
_request_json(base_url.rstrip("/") + "/models", headers, timeout)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def fetch_models(provider: str, config: Config, timeout: int = 10) -> List[Model]:
|
|
220
|
+
"""Fetch the models available from the configured AI provider."""
|
|
221
|
+
provider = provider.lower()
|
|
222
|
+
fetchers = {
|
|
223
|
+
"openrouter": _fetch_openrouter_models,
|
|
224
|
+
"gemini": _fetch_gemini_models,
|
|
225
|
+
"openai": lambda config, timeout: _models_from_openai_response(
|
|
226
|
+
_request_json(
|
|
227
|
+
"https://api.openai.com/v1/models",
|
|
228
|
+
{"Authorization": f"Bearer {_api_key(config, 'OPENAI_API_KEY')}"},
|
|
229
|
+
timeout,
|
|
230
|
+
)
|
|
231
|
+
),
|
|
232
|
+
"groq": lambda config, timeout: _models_from_openai_response(
|
|
233
|
+
_request_json(
|
|
234
|
+
"https://api.groq.com/openai/v1/models",
|
|
235
|
+
{"Authorization": f"Bearer {_api_key(config, 'GROQ_API_KEY')}"},
|
|
236
|
+
timeout,
|
|
237
|
+
)
|
|
238
|
+
),
|
|
239
|
+
"anthropic": _fetch_anthropic_models,
|
|
240
|
+
"ollama": _fetch_ollama_models,
|
|
241
|
+
"custom": _fetch_custom_models,
|
|
242
|
+
}
|
|
243
|
+
fetcher = fetchers.get(provider)
|
|
244
|
+
if not fetcher:
|
|
245
|
+
raise ModelCatalogError(f"Unsupported AI_PROVIDER: {provider}")
|
|
246
|
+
return sorted(fetcher(config, timeout), key=lambda model: str(model["id"]).casefold())
|
|
@@ -68,6 +68,9 @@ MAX_NO = 1
|
|
|
68
68
|
# Options: general, conventional, etc.
|
|
69
69
|
COMMIT_TYPE = conventional
|
|
70
70
|
|
|
71
|
+
# Reasoning: auto, none, low, medium, high, xhigh, max. Use none for faster commits.
|
|
72
|
+
REASONING_EFFORT = auto
|
|
73
|
+
|
|
71
74
|
# Gemini model to use (default: gemini-1.5-flash)
|
|
72
75
|
MODEL_NAME = gemini-1.5-flash
|
|
73
76
|
|
|
@@ -79,7 +82,7 @@ MODEL_NAME = gemini-1.5-flash
|
|
|
79
82
|
COMMIT_MODE = auto
|
|
80
83
|
|
|
81
84
|
# Files to exclude from diff (default: package-lock.json, pnpm-lock.yaml, yarn.lock, *.lock)
|
|
82
|
-
# Comma-separated
|
|
85
|
+
# Comma-separated patterns or absolute paths within one repository
|
|
83
86
|
EXCLUDE_FILES = *.lock, dist/*, build/*, node_modules/*
|
|
84
87
|
|
|
85
88
|
# Directory for changelog files (default: changelogs)
|
|
@@ -9,10 +9,12 @@ os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "1"
|
|
|
9
9
|
|
|
10
10
|
from InquirerPy import get_style, inquirer
|
|
11
11
|
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
12
13
|
|
|
13
14
|
from devcommit.__version__ import __version__
|
|
14
15
|
from devcommit.app.changelog import generate_changelog, save_changelog
|
|
15
16
|
from devcommit.app.gemini_ai import generateCommitMessage
|
|
17
|
+
from devcommit.app.model_catalog import ModelCatalogError, fetch_models
|
|
16
18
|
from devcommit.utils.git import (KnownError, assert_git_repo,
|
|
17
19
|
generate_relation_grouping_prompt,
|
|
18
20
|
get_detected_message, get_diff_for_files,
|
|
@@ -77,6 +79,49 @@ def main(flags: CommitFlag = None):
|
|
|
77
79
|
Console().print(f"devcommit version {__version__}")
|
|
78
80
|
return
|
|
79
81
|
|
|
82
|
+
if flags.get("models", False):
|
|
83
|
+
console = Console()
|
|
84
|
+
provider = config("AI_PROVIDER", default="gemini").lower()
|
|
85
|
+
try:
|
|
86
|
+
models = fetch_models(provider, config)
|
|
87
|
+
except ModelCatalogError as error:
|
|
88
|
+
console.print(f"[bold red]Error:[/bold red] {error}")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if flags.get("freeModels", False):
|
|
92
|
+
models = [model for model in models if model["availability"] == "Free"]
|
|
93
|
+
elif flags.get("paidModels", False):
|
|
94
|
+
models = [model for model in models if model["availability"] == "Paid"]
|
|
95
|
+
|
|
96
|
+
if not models:
|
|
97
|
+
console.print(f"No matching models are available from {provider}.")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
table = Table(title=f"Available {provider.title()} Models")
|
|
101
|
+
table.add_column("Model", style="bold cyan")
|
|
102
|
+
table.add_column("ID", style="green")
|
|
103
|
+
table.add_column("Context", justify="right")
|
|
104
|
+
table.add_column("Access")
|
|
105
|
+
table.add_column("Price")
|
|
106
|
+
for model in models:
|
|
107
|
+
context_length = model["context_length"]
|
|
108
|
+
context = (
|
|
109
|
+
f"{context_length:,}"
|
|
110
|
+
if isinstance(context_length, int)
|
|
111
|
+
else str(context_length or "-")
|
|
112
|
+
)
|
|
113
|
+
table.add_row(
|
|
114
|
+
str(model["name"]),
|
|
115
|
+
str(model["id"]),
|
|
116
|
+
context,
|
|
117
|
+
str(model["availability"]),
|
|
118
|
+
str(model["price"]),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
console.print(table)
|
|
122
|
+
console.print(f"\nSet one with: {provider.upper()}_MODEL='<model-id>'")
|
|
123
|
+
return
|
|
124
|
+
|
|
80
125
|
try:
|
|
81
126
|
assert_git_repo()
|
|
82
127
|
console = Console()
|
|
@@ -35,7 +35,32 @@ def exclude_from_diff(path: str) -> str:
|
|
|
35
35
|
Prepares a Git exclusion path string for the diff command.
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
return f':(exclude){path}'
|
|
38
|
+
return f':(top,exclude){path}'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_exclude_paths(paths: List[str], repo_root: str) -> List[str]:
|
|
42
|
+
"""Convert absolute exclusions in this repository to root-relative paths."""
|
|
43
|
+
normalized_paths = []
|
|
44
|
+
resolved_root = os.path.realpath(repo_root)
|
|
45
|
+
|
|
46
|
+
for path in paths:
|
|
47
|
+
expanded_path = os.path.expanduser(path)
|
|
48
|
+
if not os.path.isabs(expanded_path):
|
|
49
|
+
normalized_paths.append(path)
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
resolved_path = os.path.realpath(expanded_path)
|
|
53
|
+
try:
|
|
54
|
+
is_in_repo = os.path.commonpath(
|
|
55
|
+
[resolved_root, resolved_path]
|
|
56
|
+
) == resolved_root
|
|
57
|
+
except ValueError:
|
|
58
|
+
is_in_repo = False
|
|
59
|
+
|
|
60
|
+
if is_in_repo:
|
|
61
|
+
normalized_paths.append(os.path.relpath(resolved_path, resolved_root))
|
|
62
|
+
|
|
63
|
+
return normalized_paths
|
|
39
64
|
|
|
40
65
|
|
|
41
66
|
def get_default_excludes() -> List[str]:
|
|
@@ -70,9 +95,12 @@ def get_staged_diff(
|
|
|
70
95
|
Gets the list of staged files and their diff, excluding specified files.
|
|
71
96
|
"""
|
|
72
97
|
exclude_files = exclude_files or []
|
|
98
|
+
repo_root = assert_git_repo()
|
|
73
99
|
diff_cached = ['git', 'diff', '--cached', '--diff-algorithm=minimal']
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
excludes = normalize_exclude_paths(
|
|
101
|
+
files_to_exclude + exclude_files, repo_root
|
|
102
|
+
)
|
|
103
|
+
excluded_from_diff = [exclude_from_diff(path) for path in excludes]
|
|
76
104
|
|
|
77
105
|
try:
|
|
78
106
|
# Get the list of staged files excluding specified files
|
|
@@ -81,7 +109,8 @@ def get_staged_diff(
|
|
|
81
109
|
check=True,
|
|
82
110
|
stdout=subprocess.PIPE,
|
|
83
111
|
stderr=subprocess.PIPE,
|
|
84
|
-
text=True
|
|
112
|
+
text=True,
|
|
113
|
+
cwd=repo_root
|
|
85
114
|
)
|
|
86
115
|
files_result = (
|
|
87
116
|
files.stdout.strip().split('\n') if files.stdout.strip() else []
|
|
@@ -95,7 +124,8 @@ def get_staged_diff(
|
|
|
95
124
|
check=True,
|
|
96
125
|
stdout=subprocess.PIPE,
|
|
97
126
|
stderr=subprocess.PIPE,
|
|
98
|
-
text=True
|
|
127
|
+
text=True,
|
|
128
|
+
cwd=repo_root
|
|
99
129
|
)
|
|
100
130
|
diff_result = diff.stdout.strip()
|
|
101
131
|
|
|
@@ -140,25 +170,21 @@ def get_diff_for_files(files: List[str], exclude_files: Optional[List[str]] = No
|
|
|
140
170
|
Gets the diff for specific files.
|
|
141
171
|
"""
|
|
142
172
|
exclude_files = exclude_files or []
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if not any(f.endswith(excl.replace('*', '')) or excl.replace(':(exclude)', '') in f
|
|
149
|
-
for excl in all_excluded)
|
|
150
|
-
]
|
|
151
|
-
|
|
152
|
-
if not filtered_files:
|
|
153
|
-
return ""
|
|
173
|
+
repo_root = assert_git_repo()
|
|
174
|
+
excludes = normalize_exclude_paths(
|
|
175
|
+
files_to_exclude + exclude_files, repo_root
|
|
176
|
+
)
|
|
177
|
+
excluded_from_diff = [exclude_from_diff(path) for path in excludes]
|
|
154
178
|
|
|
155
179
|
try:
|
|
156
180
|
diff = subprocess.run(
|
|
157
|
-
['git', 'diff', '--cached', '--diff-algorithm=minimal', '--']
|
|
181
|
+
['git', 'diff', '--cached', '--diff-algorithm=minimal', '--']
|
|
182
|
+
+ files + excluded_from_diff,
|
|
158
183
|
check=True,
|
|
159
184
|
stdout=subprocess.PIPE,
|
|
160
185
|
stderr=subprocess.PIPE,
|
|
161
|
-
text=True
|
|
186
|
+
text=True,
|
|
187
|
+
cwd=repo_root
|
|
162
188
|
)
|
|
163
189
|
return diff.stdout.strip()
|
|
164
190
|
except subprocess.CalledProcessError:
|
|
@@ -12,6 +12,9 @@ class CommitFlag(TypedDict):
|
|
|
12
12
|
files: List[str]
|
|
13
13
|
push: bool
|
|
14
14
|
changelog: bool
|
|
15
|
+
models: bool
|
|
16
|
+
freeModels: bool
|
|
17
|
+
paidModels: bool
|
|
15
18
|
version: bool
|
|
16
19
|
rawArgv: List[str]
|
|
17
20
|
|
|
@@ -58,6 +61,24 @@ def parse_arguments() -> CommitFlag:
|
|
|
58
61
|
"--changelog", "-c", action="store_true",
|
|
59
62
|
help="Generate changelog file from changes"
|
|
60
63
|
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--models",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="List models from the configured AI provider and exit",
|
|
68
|
+
)
|
|
69
|
+
model_filter = parser.add_mutually_exclusive_group()
|
|
70
|
+
model_filter.add_argument(
|
|
71
|
+
"--free",
|
|
72
|
+
dest="free_models",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Only show free models with --models",
|
|
75
|
+
)
|
|
76
|
+
model_filter.add_argument(
|
|
77
|
+
"--paid",
|
|
78
|
+
dest="paid_models",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Only show paid models with --models",
|
|
81
|
+
)
|
|
61
82
|
parser.add_argument(
|
|
62
83
|
"--version", "-v", action="store_true",
|
|
63
84
|
help="Show version information"
|
|
@@ -77,6 +98,9 @@ def parse_arguments() -> CommitFlag:
|
|
|
77
98
|
files=args.files or [],
|
|
78
99
|
push=args.push,
|
|
79
100
|
changelog=args.changelog,
|
|
101
|
+
models=args.models,
|
|
102
|
+
freeModels=args.free_models,
|
|
103
|
+
paidModels=args.paid_models,
|
|
80
104
|
version=args.version,
|
|
81
105
|
rawArgv=args.rawArgv,
|
|
82
106
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "devcommit"
|
|
3
|
-
version = "0.1.5.
|
|
3
|
+
version = "0.1.5.7"
|
|
4
4
|
description = "AI-powered git commit message generator"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {file = "COPYING"}
|
|
@@ -44,7 +44,7 @@ create-dcommit = "devcommit.create_config:create_dcommit"
|
|
|
44
44
|
|
|
45
45
|
[tool.poetry]
|
|
46
46
|
name = "devcommit"
|
|
47
|
-
version = "0.1.5.
|
|
47
|
+
version = "0.1.5.7"
|
|
48
48
|
description = "AI-powered git commit message generator"
|
|
49
49
|
authors = ["Hordunlarmy <Hordunlarmy@gmail.com>"]
|
|
50
50
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|