messygit 0.1.4__tar.gz → 0.2.1__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.
- messygit-0.2.1/.github/workflows/publish.yml +27 -0
- messygit-0.2.1/ARCHITECTURE.md +48 -0
- messygit-0.2.1/PKG-INFO +146 -0
- messygit-0.2.1/README.md +136 -0
- messygit-0.2.1/messygit/agent/agent.py +79 -0
- messygit-0.2.1/messygit/agent/tool.py +28 -0
- messygit-0.2.1/messygit/agent/tools.py +82 -0
- messygit-0.2.1/messygit/cli.py +283 -0
- {messygit-0.1.4 → messygit-0.2.1}/messygit/git.py +16 -0
- {messygit-0.1.4 → messygit-0.2.1}/messygit/prompts.py +38 -0
- {messygit-0.1.4 → messygit-0.2.1}/pyproject.toml +2 -2
- messygit-0.1.4/PKG-INFO +0 -115
- messygit-0.1.4/README.md +0 -105
- messygit-0.1.4/messygit/cli.py +0 -112
- {messygit-0.1.4 → messygit-0.2.1}/.gitignore +0 -0
- {messygit-0.1.4 → messygit-0.2.1}/messygit/__init__.py +0 -0
- {messygit-0.1.4 → messygit-0.2.1}/messygit/config.py +0 -0
- {messygit-0.1.4 → messygit-0.2.1}/messygit/llm.py +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.13"
|
|
19
|
+
|
|
20
|
+
- name: Install build tools
|
|
21
|
+
run: pip install build
|
|
22
|
+
|
|
23
|
+
- name: Build package
|
|
24
|
+
run: python -m build
|
|
25
|
+
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This document describes the purpose of each file in the `messygit` project.
|
|
4
|
+
|
|
5
|
+
## Root
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
| File | Purpose |
|
|
9
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
10
|
+
| `pyproject.toml` | Package metadata, dependencies (`anthropic`, `click`), build system (hatchling), and the `messygit` console script entrypoint. |
|
|
11
|
+
| `README.md` | User-facing documentation: install, usage, commands, and development instructions. |
|
|
12
|
+
| `.gitignore` | Keeps `.venv/`, `__pycache__/`, `dist/`, and `*.egg-info/` out of version control. |
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## `messygit/` (Python package)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
| File | Purpose |
|
|
19
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
20
|
+
| `__init__.py` | Marks the directory as a Python package (empty). |
|
|
21
|
+
| `cli.py` | Click CLI entrypoint. Defines the command group (`main`), the default commit flow (generate → prompt Y/n/e → commit), and subcommands (`config`, `show`). Orchestrates all other modules. |
|
|
22
|
+
| `git.py` | All subprocess calls to `git`. Reads staged diffs (`git diff --cached -U0`), parses them into a compact changed-lines format, filters noise files, handles the large-diff fallback (stat summary + top-N most-changed files), and runs `git commit -m`. |
|
|
23
|
+
| `llm.py` | Anthropic SDK integration. Creates the client with the resolved API key, calls `messages.create`, extracts the text response, and maps SDK exceptions (`AuthenticationError`, `PermissionDeniedError`, `BadRequestError`, billing 402) into user-friendly error classes. |
|
|
24
|
+
| `config.py` | API key storage and resolution. Reads/writes `~/.messygit/config.json`, checks the `ANTHROPIC_API_KEY` env var, validates keys are non-empty, masks keys for display, and defines all user-facing error messages and exception classes. |
|
|
25
|
+
| `prompts.py` | System prompt and user prompt builder. Contains the full Conventional Commits instructions, input format descriptions (full and truncated), security rules, and the function that wraps staged changes into the user message sent to Claude. |
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Data flow
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
User runs `messygit`
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
cli.py ──► git.py (read staged diff, apply token budget)
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
cli.py ──► llm.py (send context to Claude)
|
|
38
|
+
│ │
|
|
39
|
+
│ ├── config.py (resolve API key)
|
|
40
|
+
│ └── prompts.py (system + user prompt)
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
cli.py (display message, prompt Y/n/e)
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
cli.py ──► git.py (git commit -m "...")
|
|
47
|
+
```
|
|
48
|
+
|
messygit-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: messygit
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: An AI-powered interactive git CLI with agentic tools for commits, code suggestions, and workflow automation.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: anthropic>=0.39.0
|
|
8
|
+
Requires-Dist: click>=8.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# messygit
|
|
12
|
+
|
|
13
|
+
**messygit** is an interactive CLI that turns messy git workflows into clean Conventional Commits — stage, commit, push, and get AI-powered project suggestions, all from one interface powered by [Claude](https://www.anthropic.com/api).
|
|
14
|
+
|
|
15
|
+
## Why use it
|
|
16
|
+
|
|
17
|
+
- **Interactive REPL** — one command drops you into a persistent session where you can stage, commit, push, and more without leaving.
|
|
18
|
+
- **AI commit messages** — sends your staged diff to Claude and suggests a clean Conventional Commits subject line.
|
|
19
|
+
- **Project suggestions** — an AI agent inspects your repo and recommends concrete next steps.
|
|
20
|
+
- **Safe by default** — only the staged diff is sent to the model. Your API key is never printed in full.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- **Python** 3.10 or newer
|
|
25
|
+
- **Git** (run inside a repository)
|
|
26
|
+
- An **Anthropic API key** with access to the Messages API
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install messygit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Install from source
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd messygit
|
|
38
|
+
python -m venv .venv
|
|
39
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
40
|
+
pip install -e .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API key
|
|
44
|
+
|
|
45
|
+
messygit resolves the key in this order:
|
|
46
|
+
|
|
47
|
+
1. Environment variable **`ANTHROPIC_API_KEY`**
|
|
48
|
+
2. Config file **`~/.messygit/config.json`**
|
|
49
|
+
|
|
50
|
+
You can set the key from within the messygit interface:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
messygit > config YOUR_ANTHROPIC_API_KEY
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
messygit
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This drops you into the interactive interface:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
mmm mmm eeeeeee sssssss sssssss yy yy ggggggg ii tttttttt
|
|
66
|
+
mm mm mm m ee ss ss yy yy gg ii tt
|
|
67
|
+
mm mmm m eeeee sssssss sssssss yyy gg ggg ii tt
|
|
68
|
+
mm m ee ss ss yy gg gg ii tt
|
|
69
|
+
mm m eeeeeee sssssss sssssss yy ggggg ii tt
|
|
70
|
+
|
|
71
|
+
Type 'help' for commands, 'quit' to exit.
|
|
72
|
+
|
|
73
|
+
messygit >
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Commands
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---------|-------------|
|
|
80
|
+
| `add <file>` or `add .` | Stage files for commit |
|
|
81
|
+
| `commit` | Generate an AI commit message from staged changes, then commit / cancel / edit |
|
|
82
|
+
| `push` | Push commits to remote |
|
|
83
|
+
| `suggestion` | Get AI-powered next-step suggestions for your project |
|
|
84
|
+
| `config <key>` | Save your Anthropic API key to `~/.messygit/config.json` |
|
|
85
|
+
| `show` | Display a masked API key and its source |
|
|
86
|
+
| `help` | List available commands |
|
|
87
|
+
| `quit` / `exit` | Exit messygit |
|
|
88
|
+
|
|
89
|
+
### Typical flow
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
messygit > add .
|
|
93
|
+
Staged everything
|
|
94
|
+
|
|
95
|
+
messygit > commit
|
|
96
|
+
feat(cli): add interactive REPL with ASCII banner
|
|
97
|
+
Commit with this message? [y/n/e] y
|
|
98
|
+
|
|
99
|
+
messygit > push
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Commit message style
|
|
103
|
+
|
|
104
|
+
The model follows **Conventional Commits**: `type(scope): description`
|
|
105
|
+
|
|
106
|
+
Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Subjects are one line, imperative, lowercase, no trailing period.
|
|
107
|
+
|
|
108
|
+
## Publishing to PyPI
|
|
109
|
+
|
|
110
|
+
This project uses GitHub Actions with [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) — no API tokens needed in your repo.
|
|
111
|
+
|
|
112
|
+
### One-time setup
|
|
113
|
+
|
|
114
|
+
1. Go to your project on [pypi.org](https://pypi.org/manage/project/messygit/settings/publishing/)
|
|
115
|
+
2. Add a **Trusted Publisher**:
|
|
116
|
+
- **Owner:** your GitHub username
|
|
117
|
+
- **Repository:** `messygit`
|
|
118
|
+
- **Workflow name:** `publish.yml`
|
|
119
|
+
- **Environment:** leave blank
|
|
120
|
+
|
|
121
|
+
### To release a new version
|
|
122
|
+
|
|
123
|
+
1. Bump `version` in `pyproject.toml`
|
|
124
|
+
2. Commit and push
|
|
125
|
+
3. Create a GitHub release:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git tag v0.2.0
|
|
129
|
+
git push origin v0.2.0
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
4. Go to GitHub → Releases → Draft a new release → select the tag → Publish
|
|
133
|
+
|
|
134
|
+
The workflow at `.github/workflows/publish.yml` will automatically build and upload to PyPI.
|
|
135
|
+
|
|
136
|
+
### Manual publish (without CI)
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
rm -rf dist/
|
|
140
|
+
python -m build
|
|
141
|
+
twine upload dist/*
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT (see `pyproject.toml`).
|
messygit-0.2.1/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# messygit
|
|
2
|
+
|
|
3
|
+
**messygit** is an interactive CLI that turns messy git workflows into clean Conventional Commits — stage, commit, push, and get AI-powered project suggestions, all from one interface powered by [Claude](https://www.anthropic.com/api).
|
|
4
|
+
|
|
5
|
+
## Why use it
|
|
6
|
+
|
|
7
|
+
- **Interactive REPL** — one command drops you into a persistent session where you can stage, commit, push, and more without leaving.
|
|
8
|
+
- **AI commit messages** — sends your staged diff to Claude and suggests a clean Conventional Commits subject line.
|
|
9
|
+
- **Project suggestions** — an AI agent inspects your repo and recommends concrete next steps.
|
|
10
|
+
- **Safe by default** — only the staged diff is sent to the model. Your API key is never printed in full.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- **Python** 3.10 or newer
|
|
15
|
+
- **Git** (run inside a repository)
|
|
16
|
+
- An **Anthropic API key** with access to the Messages API
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install messygit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Install from source
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd messygit
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API key
|
|
34
|
+
|
|
35
|
+
messygit resolves the key in this order:
|
|
36
|
+
|
|
37
|
+
1. Environment variable **`ANTHROPIC_API_KEY`**
|
|
38
|
+
2. Config file **`~/.messygit/config.json`**
|
|
39
|
+
|
|
40
|
+
You can set the key from within the messygit interface:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
messygit > config YOUR_ANTHROPIC_API_KEY
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
messygit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This drops you into the interactive interface:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
mmm mmm eeeeeee sssssss sssssss yy yy ggggggg ii tttttttt
|
|
56
|
+
mm mm mm m ee ss ss yy yy gg ii tt
|
|
57
|
+
mm mmm m eeeee sssssss sssssss yyy gg ggg ii tt
|
|
58
|
+
mm m ee ss ss yy gg gg ii tt
|
|
59
|
+
mm m eeeeeee sssssss sssssss yy ggggg ii tt
|
|
60
|
+
|
|
61
|
+
Type 'help' for commands, 'quit' to exit.
|
|
62
|
+
|
|
63
|
+
messygit >
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Commands
|
|
67
|
+
|
|
68
|
+
| Command | Description |
|
|
69
|
+
|---------|-------------|
|
|
70
|
+
| `add <file>` or `add .` | Stage files for commit |
|
|
71
|
+
| `commit` | Generate an AI commit message from staged changes, then commit / cancel / edit |
|
|
72
|
+
| `push` | Push commits to remote |
|
|
73
|
+
| `suggestion` | Get AI-powered next-step suggestions for your project |
|
|
74
|
+
| `config <key>` | Save your Anthropic API key to `~/.messygit/config.json` |
|
|
75
|
+
| `show` | Display a masked API key and its source |
|
|
76
|
+
| `help` | List available commands |
|
|
77
|
+
| `quit` / `exit` | Exit messygit |
|
|
78
|
+
|
|
79
|
+
### Typical flow
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
messygit > add .
|
|
83
|
+
Staged everything
|
|
84
|
+
|
|
85
|
+
messygit > commit
|
|
86
|
+
feat(cli): add interactive REPL with ASCII banner
|
|
87
|
+
Commit with this message? [y/n/e] y
|
|
88
|
+
|
|
89
|
+
messygit > push
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Commit message style
|
|
93
|
+
|
|
94
|
+
The model follows **Conventional Commits**: `type(scope): description`
|
|
95
|
+
|
|
96
|
+
Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Subjects are one line, imperative, lowercase, no trailing period.
|
|
97
|
+
|
|
98
|
+
## Publishing to PyPI
|
|
99
|
+
|
|
100
|
+
This project uses GitHub Actions with [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) — no API tokens needed in your repo.
|
|
101
|
+
|
|
102
|
+
### One-time setup
|
|
103
|
+
|
|
104
|
+
1. Go to your project on [pypi.org](https://pypi.org/manage/project/messygit/settings/publishing/)
|
|
105
|
+
2. Add a **Trusted Publisher**:
|
|
106
|
+
- **Owner:** your GitHub username
|
|
107
|
+
- **Repository:** `messygit`
|
|
108
|
+
- **Workflow name:** `publish.yml`
|
|
109
|
+
- **Environment:** leave blank
|
|
110
|
+
|
|
111
|
+
### To release a new version
|
|
112
|
+
|
|
113
|
+
1. Bump `version` in `pyproject.toml`
|
|
114
|
+
2. Commit and push
|
|
115
|
+
3. Create a GitHub release:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git tag v0.2.0
|
|
119
|
+
git push origin v0.2.0
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
4. Go to GitHub → Releases → Draft a new release → select the tag → Publish
|
|
123
|
+
|
|
124
|
+
The workflow at `.github/workflows/publish.yml` will automatically build and upload to PyPI.
|
|
125
|
+
|
|
126
|
+
### Manual publish (without CI)
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
rm -rf dist/
|
|
130
|
+
python -m build
|
|
131
|
+
twine upload dist/*
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT (see `pyproject.toml`).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from .tool import Tool
|
|
2
|
+
from anthropic import (
|
|
3
|
+
Anthropic,
|
|
4
|
+
APIStatusError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
BadRequestError,
|
|
7
|
+
PermissionDeniedError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ..config import (
|
|
11
|
+
FORBIDDEN_API_KEY_MESSAGE,
|
|
12
|
+
INVALID_API_KEY_MESSAGE,
|
|
13
|
+
AnthropicInsufficientBalanceError,
|
|
14
|
+
InvalidAnthropicCredentialsError,
|
|
15
|
+
resolve_api_key,
|
|
16
|
+
)
|
|
17
|
+
from ..llm import _is_insufficient_balance_or_billing, _insufficient_balance_user_message, _text_from_message
|
|
18
|
+
|
|
19
|
+
DEFAULT_MODEL = "claude-haiku-4-5-20251001"
|
|
20
|
+
DEFAULT_MAX_TOKENS = 4096
|
|
21
|
+
|
|
22
|
+
class Agent:
|
|
23
|
+
def __init__(self, name: str, system_prompt: str, max_iterations: int, tools: list[Tool]):
|
|
24
|
+
self.name = name
|
|
25
|
+
self.system_prompt = system_prompt
|
|
26
|
+
self.max_iterations = max_iterations
|
|
27
|
+
self.tools = tools
|
|
28
|
+
|
|
29
|
+
def run(self, user_input: str) -> str:
|
|
30
|
+
"""Run the agent."""
|
|
31
|
+
client = Anthropic(api_key=resolve_api_key())
|
|
32
|
+
messages = []
|
|
33
|
+
try:
|
|
34
|
+
messages.append({"role": "user", "content": user_input})
|
|
35
|
+
response = None
|
|
36
|
+
for i in range(self.max_iterations):
|
|
37
|
+
response = client.messages.create(
|
|
38
|
+
model=DEFAULT_MODEL,
|
|
39
|
+
max_tokens=DEFAULT_MAX_TOKENS,
|
|
40
|
+
tools=[t.to_schema() for t in self.tools],
|
|
41
|
+
tool_choice={"type": "auto"},
|
|
42
|
+
system=self.system_prompt,
|
|
43
|
+
messages=messages,
|
|
44
|
+
)
|
|
45
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
46
|
+
|
|
47
|
+
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
|
|
48
|
+
if not tool_use_blocks:
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
tool_results = []
|
|
52
|
+
for block in tool_use_blocks:
|
|
53
|
+
tool = next(t for t in self.tools if t.name == block.name)
|
|
54
|
+
result = tool.run(**block.input)
|
|
55
|
+
tool_results.append({
|
|
56
|
+
"type": "tool_result",
|
|
57
|
+
"tool_use_id": block.id,
|
|
58
|
+
"content": str(result),
|
|
59
|
+
})
|
|
60
|
+
messages.append({"role": "user", "content": tool_results})
|
|
61
|
+
except AuthenticationError as e:
|
|
62
|
+
raise InvalidAnthropicCredentialsError(INVALID_API_KEY_MESSAGE) from e
|
|
63
|
+
except PermissionDeniedError as e:
|
|
64
|
+
raise InvalidAnthropicCredentialsError(FORBIDDEN_API_KEY_MESSAGE) from e
|
|
65
|
+
except BadRequestError as e:
|
|
66
|
+
if _is_insufficient_balance_or_billing(e):
|
|
67
|
+
raise AnthropicInsufficientBalanceError(
|
|
68
|
+
_insufficient_balance_user_message(e)
|
|
69
|
+
) from e
|
|
70
|
+
raise
|
|
71
|
+
except APIStatusError as e:
|
|
72
|
+
if _is_insufficient_balance_or_billing(e):
|
|
73
|
+
raise AnthropicInsufficientBalanceError(
|
|
74
|
+
_insufficient_balance_user_message(e)
|
|
75
|
+
) from e
|
|
76
|
+
raise
|
|
77
|
+
if not response:
|
|
78
|
+
return "No response from the agent."
|
|
79
|
+
return _text_from_message(response)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Tool:
|
|
9
|
+
"""A tool that an Agent can invoke, wrapping a plain Python function."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
description: str
|
|
13
|
+
function: Callable[..., Any]
|
|
14
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
def run(self, **kwargs: Any) -> Any:
|
|
17
|
+
return self.function(**kwargs)
|
|
18
|
+
|
|
19
|
+
def to_schema(self) -> dict[str, Any]:
|
|
20
|
+
"""Return an Anthropic-compatible tool schema for API calls."""
|
|
21
|
+
return {
|
|
22
|
+
"name": self.name,
|
|
23
|
+
"description": self.description,
|
|
24
|
+
"input_schema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": self.parameters,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .tool import Tool
|
|
3
|
+
from ..git import get_staged_diff, get_staged_files
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
ALLOWED_GIT_COMMANDS = ["log", "diff", "status", "show", "status", "shortlog", "blame"]
|
|
7
|
+
|
|
8
|
+
def run_git(args: list[str]) -> str:
|
|
9
|
+
if not args or args[0] not in ALLOWED_GIT_COMMANDS:
|
|
10
|
+
return "Invalid git command."
|
|
11
|
+
result = subprocess.run(["git", *args], capture_output=True, text=True)
|
|
12
|
+
return result.stdout or result.stderr
|
|
13
|
+
|
|
14
|
+
run_git_tool = Tool(
|
|
15
|
+
name="run_git",
|
|
16
|
+
description="Run a git command",
|
|
17
|
+
function=run_git,
|
|
18
|
+
parameters={
|
|
19
|
+
"args": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {"type": "string"},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def read_file(path: str) -> str:
|
|
27
|
+
try:
|
|
28
|
+
with open(path, "r") as file:
|
|
29
|
+
return file.read()
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
return "File not found."
|
|
32
|
+
except PermissionError:
|
|
33
|
+
return "Permission denied."
|
|
34
|
+
except Exception as e:
|
|
35
|
+
return f"Error reading file: {e}"
|
|
36
|
+
|
|
37
|
+
read_file_tool = Tool(
|
|
38
|
+
name="read_file",
|
|
39
|
+
description="Read a file",
|
|
40
|
+
function=read_file,
|
|
41
|
+
parameters={
|
|
42
|
+
"path": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def list_directory(path: str) -> list[str]:
|
|
49
|
+
try:
|
|
50
|
+
return os.listdir(path)
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
return "Directory not found."
|
|
53
|
+
except PermissionError:
|
|
54
|
+
return "Permission denied."
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return f"Error listing directory: {e}"
|
|
57
|
+
|
|
58
|
+
list_directory_tool = Tool(
|
|
59
|
+
name="list_directory",
|
|
60
|
+
description="List a directory",
|
|
61
|
+
function=list_directory,
|
|
62
|
+
parameters={
|
|
63
|
+
"path": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def search_code(query: str) -> str:
|
|
70
|
+
result = subprocess.run(["git", "grep", "-n", query], capture_output=True, text=True)
|
|
71
|
+
return result.stdout or result.stderr
|
|
72
|
+
|
|
73
|
+
search_code_tool = Tool(
|
|
74
|
+
name="search_code",
|
|
75
|
+
description="Search the codebase for a query",
|
|
76
|
+
function=search_code,
|
|
77
|
+
parameters={
|
|
78
|
+
"query": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shlex
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .config import (
|
|
10
|
+
ANTHROPIC_ENV_VAR,
|
|
11
|
+
CONFIG_FILE,
|
|
12
|
+
AnthropicInsufficientBalanceError,
|
|
13
|
+
InvalidAnthropicCredentialsError,
|
|
14
|
+
MissingApiKeyError,
|
|
15
|
+
load_api_key,
|
|
16
|
+
mask_api_key,
|
|
17
|
+
save_api_key,
|
|
18
|
+
)
|
|
19
|
+
from .git import get_staged_diff, git_add, git_commit, git_push
|
|
20
|
+
from .llm import generate_commit_message
|
|
21
|
+
from .prompts import SUGGESTION_SYSTEM_PROMPT
|
|
22
|
+
from .agent.tools import run_git_tool, read_file_tool, list_directory_tool, search_code_tool
|
|
23
|
+
from .agent.agent import Agent
|
|
24
|
+
|
|
25
|
+
BANNER = r"""
|
|
26
|
+
=========================================================================
|
|
27
|
+
mmm mmmm eeeeeee sssssss sssssss yy yy ggggggg ii tttttttt
|
|
28
|
+
mm mm mm mm ee ss ss yy yy gg ii tt
|
|
29
|
+
mm mmm mm eeeee sssssss sssssss yy gg ggg ii tt
|
|
30
|
+
mm mm ee ss ss yy gg gg ii tt
|
|
31
|
+
mm mm eeeeeee sssssss sssssss yy ggggg ii tt
|
|
32
|
+
=========================================================================
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
HELP_TEXT = """
|
|
36
|
+
commands:
|
|
37
|
+
add stage files (usage: add . or add <file> ...)
|
|
38
|
+
commit generate a commit message from staged changes
|
|
39
|
+
push push commits to remote
|
|
40
|
+
config set your Anthropic API key (usage: config <key>)
|
|
41
|
+
show display your masked API key
|
|
42
|
+
suggest suggest next steps for your project
|
|
43
|
+
help show this help message
|
|
44
|
+
quit/exit exit messygit
|
|
45
|
+
""".strip()
|
|
46
|
+
|
|
47
|
+
SPINNER_PHRASES = [
|
|
48
|
+
"brewing commit magic",
|
|
49
|
+
"reading your diffs",
|
|
50
|
+
"thinking real hard",
|
|
51
|
+
"untangling your code",
|
|
52
|
+
"consulting the git gods",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Spinner:
|
|
57
|
+
"""Animated loading indicator that runs in a background thread."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, phrase: str | None = None):
|
|
60
|
+
import random
|
|
61
|
+
self._phrase = phrase or random.choice(SPINNER_PHRASES)
|
|
62
|
+
self._stop = threading.Event()
|
|
63
|
+
self._thread: threading.Thread | None = None
|
|
64
|
+
|
|
65
|
+
def _animate(self) -> None:
|
|
66
|
+
dots = [" ", ". ", ".. ", "..."]
|
|
67
|
+
idx = 0
|
|
68
|
+
while not self._stop.is_set():
|
|
69
|
+
frame = f"\r {self._phrase} {dots[idx % len(dots)]}"
|
|
70
|
+
sys.stderr.write(frame)
|
|
71
|
+
sys.stderr.flush()
|
|
72
|
+
idx += 1
|
|
73
|
+
self._stop.wait(0.4)
|
|
74
|
+
sys.stderr.write("\r" + " " * (len(self._phrase) + 10) + "\r")
|
|
75
|
+
sys.stderr.flush()
|
|
76
|
+
|
|
77
|
+
def __enter__(self):
|
|
78
|
+
self._thread = threading.Thread(target=self._animate, daemon=True)
|
|
79
|
+
self._thread.start()
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, *exc):
|
|
83
|
+
self._stop.set()
|
|
84
|
+
if self._thread:
|
|
85
|
+
self._thread.join()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _print_error(msg: str) -> None:
|
|
89
|
+
click.secho(f"error: {msg}", fg="red", err=True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _prompt_commit_action(message: str) -> None:
|
|
93
|
+
"""Ask [Y/n/e]: commit, cancel, or edit in $EDITOR."""
|
|
94
|
+
current = message
|
|
95
|
+
while True:
|
|
96
|
+
click.echo(current)
|
|
97
|
+
choice = click.prompt(
|
|
98
|
+
"Commit with this message? [y/n/e]",
|
|
99
|
+
default="Y",
|
|
100
|
+
show_default=False,
|
|
101
|
+
).strip().lower()
|
|
102
|
+
|
|
103
|
+
if choice in ("", "y", "yes"):
|
|
104
|
+
result = git_commit(current)
|
|
105
|
+
if result.stdout:
|
|
106
|
+
click.echo(result.stdout, nl=False)
|
|
107
|
+
if result.stderr:
|
|
108
|
+
click.echo(result.stderr, nl=False, err=True)
|
|
109
|
+
if result.returncode != 0:
|
|
110
|
+
_print_error("git commit failed.")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if choice in ("n", "no"):
|
|
114
|
+
click.echo("Commit cancelled.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if choice in ("e", "edit"):
|
|
118
|
+
edited = click.edit(current)
|
|
119
|
+
if edited is None:
|
|
120
|
+
click.echo("Editor exited without saving; message unchanged.")
|
|
121
|
+
continue
|
|
122
|
+
stripped = edited.strip()
|
|
123
|
+
if not stripped:
|
|
124
|
+
click.echo("Empty message ignored; message unchanged.")
|
|
125
|
+
continue
|
|
126
|
+
current = stripped
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
click.echo("Please answer y (yes), n (no), or e (edit).")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _handle_add(args: list[str]) -> None:
|
|
133
|
+
if not args:
|
|
134
|
+
_print_error("Usage: add <file> ... or add .")
|
|
135
|
+
return
|
|
136
|
+
result = git_add(args)
|
|
137
|
+
if result.returncode != 0:
|
|
138
|
+
_print_error(result.stderr.strip() if result.stderr else "git add failed.")
|
|
139
|
+
return
|
|
140
|
+
label = "everything" if args == ["."] else ", ".join(args)
|
|
141
|
+
click.echo(f"Staged {label}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _handle_push() -> None:
|
|
145
|
+
result = git_push()
|
|
146
|
+
if result.returncode != 0:
|
|
147
|
+
_print_error(result.stderr.strip() if result.stderr else "git push failed.")
|
|
148
|
+
return
|
|
149
|
+
output = (result.stdout or result.stderr or "").strip()
|
|
150
|
+
if output:
|
|
151
|
+
click.echo(output)
|
|
152
|
+
else:
|
|
153
|
+
click.echo("Pushed successfully.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _handle_commit() -> None:
|
|
157
|
+
diff = get_staged_diff()
|
|
158
|
+
if not diff.strip():
|
|
159
|
+
_print_error("No staged changes found. Run 'add .' or 'add <file>' first.")
|
|
160
|
+
return
|
|
161
|
+
try:
|
|
162
|
+
with Spinner():
|
|
163
|
+
message = generate_commit_message(diff)
|
|
164
|
+
except (MissingApiKeyError, InvalidAnthropicCredentialsError, AnthropicInsufficientBalanceError) as e:
|
|
165
|
+
_print_error(str(e))
|
|
166
|
+
return
|
|
167
|
+
_prompt_commit_action(message)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _handle_config(args: list[str]) -> None:
|
|
171
|
+
if not args:
|
|
172
|
+
_print_error("Usage: config <api-key>")
|
|
173
|
+
return
|
|
174
|
+
key = args[0]
|
|
175
|
+
try:
|
|
176
|
+
save_api_key(key)
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
_print_error(str(e))
|
|
179
|
+
return
|
|
180
|
+
click.echo(f"API key saved successfully ({mask_api_key(key.strip())})")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _handle_show() -> None:
|
|
184
|
+
env_set = ANTHROPIC_ENV_VAR in os.environ
|
|
185
|
+
env_key = (os.environ.get(ANTHROPIC_ENV_VAR) or "").strip()
|
|
186
|
+
if env_key:
|
|
187
|
+
click.echo(f"API key: {mask_api_key(env_key)} (from ANTHROPIC_API_KEY)")
|
|
188
|
+
return
|
|
189
|
+
file_key = load_api_key()
|
|
190
|
+
if file_key:
|
|
191
|
+
if env_set:
|
|
192
|
+
click.echo(
|
|
193
|
+
f"{ANTHROPIC_ENV_VAR} is set but empty; showing key from {CONFIG_FILE}."
|
|
194
|
+
)
|
|
195
|
+
click.echo(f"API key: {mask_api_key(file_key)} (from {CONFIG_FILE})")
|
|
196
|
+
return
|
|
197
|
+
if env_set:
|
|
198
|
+
click.echo(
|
|
199
|
+
f"{ANTHROPIC_ENV_VAR} is set but empty or whitespace-only, and no usable key "
|
|
200
|
+
f"is stored in {CONFIG_FILE}. Unset the variable or run: config <key>"
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
click.echo("No API key found. Set ANTHROPIC_API_KEY or run: config <key>")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _handle_suggestion() -> None:
|
|
207
|
+
agent = Agent(
|
|
208
|
+
name="suggestion_agent",
|
|
209
|
+
system_prompt=SUGGESTION_SYSTEM_PROMPT,
|
|
210
|
+
max_iterations=8,
|
|
211
|
+
tools=[run_git_tool, read_file_tool, list_directory_tool],
|
|
212
|
+
)
|
|
213
|
+
try:
|
|
214
|
+
with Spinner():
|
|
215
|
+
result = agent.run("What should the next steps for my project be? Let's limit it to 3-5 steps")
|
|
216
|
+
except (MissingApiKeyError, InvalidAnthropicCredentialsError, AnthropicInsufficientBalanceError) as e:
|
|
217
|
+
_print_error(str(e))
|
|
218
|
+
return
|
|
219
|
+
click.echo(result)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
COMMANDS = {
|
|
223
|
+
"add": _handle_add,
|
|
224
|
+
"commit": lambda args: _handle_commit(),
|
|
225
|
+
"push": lambda args: _handle_push(),
|
|
226
|
+
"config": _handle_config,
|
|
227
|
+
"show": lambda args: _handle_show(),
|
|
228
|
+
"suggest": lambda args: _handle_suggestion(),
|
|
229
|
+
"help": lambda args: click.echo(HELP_TEXT),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _repl() -> None:
|
|
234
|
+
click.echo()
|
|
235
|
+
click.secho(BANNER, fg="cyan", bold=True)
|
|
236
|
+
click.echo()
|
|
237
|
+
click.echo("Type 'help' for commands, 'quit' to exit.")
|
|
238
|
+
click.echo()
|
|
239
|
+
|
|
240
|
+
while True:
|
|
241
|
+
try:
|
|
242
|
+
raw = click.prompt(
|
|
243
|
+
click.style("messygit", fg="cyan", bold=True) + click.style(" > ", bold=True),
|
|
244
|
+
prompt_suffix="",
|
|
245
|
+
default="",
|
|
246
|
+
show_default=False,
|
|
247
|
+
).strip()
|
|
248
|
+
except (EOFError, KeyboardInterrupt):
|
|
249
|
+
click.echo()
|
|
250
|
+
click.secho("Bye!", fg="cyan")
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
if not raw:
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
parts = shlex.split(raw)
|
|
258
|
+
except ValueError:
|
|
259
|
+
parts = raw.split()
|
|
260
|
+
|
|
261
|
+
cmd, args = parts[0].lower(), parts[1:]
|
|
262
|
+
|
|
263
|
+
if cmd in ("quit", "exit"):
|
|
264
|
+
click.secho("Bye!", fg="cyan")
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
handler = COMMANDS.get(cmd)
|
|
268
|
+
if handler is None:
|
|
269
|
+
_print_error(f"Unknown command '{cmd}'. Type 'help' for a list of commands.")
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
handler(args)
|
|
273
|
+
click.echo()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@click.command()
|
|
277
|
+
def main():
|
|
278
|
+
"""Messy Git — interactive CLI for clean commits from messy code."""
|
|
279
|
+
_repl()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
main()
|
|
@@ -232,6 +232,22 @@ def get_staged_files() -> list[str]:
|
|
|
232
232
|
return [f for f in files.split("\n") if not _is_noise_file(f)]
|
|
233
233
|
|
|
234
234
|
|
|
235
|
+
def git_add(paths: list[str]) -> CompletedProcess[str]:
|
|
236
|
+
return subprocess.run(
|
|
237
|
+
["git", "add", *paths],
|
|
238
|
+
capture_output=True,
|
|
239
|
+
text=True,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def git_push() -> CompletedProcess[str]:
|
|
244
|
+
return subprocess.run(
|
|
245
|
+
["git", "push"],
|
|
246
|
+
capture_output=True,
|
|
247
|
+
text=True,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
235
251
|
def git_commit(message: str) -> CompletedProcess[str]:
|
|
236
252
|
return subprocess.run(
|
|
237
253
|
["git", "commit", "-m", message],
|
|
@@ -60,6 +60,44 @@ YOU MUST:
|
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
SUGGESTION_SYSTEM_PROMPT = """\
|
|
64
|
+
You are a senior developer reviewing a git repository to suggest actionable \
|
|
65
|
+
next steps. You have tools to inspect the repo — use them to understand the \
|
|
66
|
+
codebase before responding.
|
|
67
|
+
|
|
68
|
+
# Workflow
|
|
69
|
+
1. Run git status, git log, and list the directory to understand the current state.
|
|
70
|
+
2. Read key files (README, config, entry points) to understand the project's purpose.
|
|
71
|
+
3. Identify what's been done recently and what gaps or opportunities remain.
|
|
72
|
+
|
|
73
|
+
# Output format (strict)
|
|
74
|
+
Respond with a SHORT summary (1-2 sentences) of what the project is and where \
|
|
75
|
+
it stands, followed by a numbered list of 3-5 concrete next steps.
|
|
76
|
+
|
|
77
|
+
Each step must be:
|
|
78
|
+
- One line, imperative mood ("Add tests for…", "Refactor…", "Set up…")
|
|
79
|
+
- Specific enough to act on immediately (name files, modules, or concepts)
|
|
80
|
+
- Ordered by priority (most impactful first)
|
|
81
|
+
|
|
82
|
+
Example output:
|
|
83
|
+
|
|
84
|
+
A CLI tool for generating commit messages from diffs — core functionality works, \
|
|
85
|
+
but lacks tests and error handling polish.
|
|
86
|
+
|
|
87
|
+
1. Add unit tests for the diff parser in git.py
|
|
88
|
+
2. Handle the edge case where git is not installed
|
|
89
|
+
3. Add a --dry-run flag to preview without committing
|
|
90
|
+
4. Write a README with install and usage instructions
|
|
91
|
+
5. Set up CI with GitHub Actions
|
|
92
|
+
|
|
93
|
+
# Rules
|
|
94
|
+
- No markdown headers, bold, or code fences in the final output.
|
|
95
|
+
- No filler phrases ("Here are some suggestions", "I'd recommend").
|
|
96
|
+
- Jump straight into the summary and list.
|
|
97
|
+
- Keep total output under 15 lines.\
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
63
101
|
def build_user_prompt(staged_changes: str) -> str:
|
|
64
102
|
return (
|
|
65
103
|
"Generate a commit message for the following staged changes.\n"
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "messygit"
|
|
7
|
-
version = "0.1
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "An AI-powered interactive git CLI with agentic tools for commits, code suggestions, and workflow automation."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
messygit-0.1.4/PKG-INFO
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: messygit
|
|
3
|
-
Version: 0.1.4
|
|
4
|
-
Summary: CLI that drafts Conventional Commits from staged git diffs with Claude, then commit, cancel, or edit.
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Requires-Dist: anthropic>=0.39.0
|
|
8
|
-
Requires-Dist: click>=8.0
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
|
|
11
|
-
# messygit
|
|
12
|
-
|
|
13
|
-
**messygit** is a command-line tool that reads your **staged** Git changes, asks **Claude** (via the [Anthropic API](https://www.anthropic.com/api)) to suggest a **Conventional Commits** subject line, and then lets you **commit**, **cancel**, or **edit** the message before running `git commit`.
|
|
14
|
-
|
|
15
|
-
## Why use it
|
|
16
|
-
|
|
17
|
-
- Keeps commit subjects consistent (`feat(scope): describe the change`) without thinking up wording from scratch.
|
|
18
|
-
- Only the **staged** diff is sent to the model—what you `git add` is what gets summarized.
|
|
19
|
-
- The API key is never printed in full; `show` uses a masked preview.
|
|
20
|
-
- Clear errors for missing keys, rejected keys, and billing or zero-balance situations.
|
|
21
|
-
|
|
22
|
-
## Requirements
|
|
23
|
-
|
|
24
|
-
- **Python** 3.10 or newer
|
|
25
|
-
- **Git** (run inside a repository)
|
|
26
|
-
- An **Anthropic API key** with access to the Messages API
|
|
27
|
-
|
|
28
|
-
## Installation
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
pip install messygit
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
This installs the `messygit` command (see `[project.scripts]` in `pyproject.toml`).
|
|
35
|
-
|
|
36
|
-
### Install from source
|
|
37
|
-
|
|
38
|
-
From a checkout of this project:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
cd messygit
|
|
42
|
-
python -m venv .venv
|
|
43
|
-
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
44
|
-
pip install -e .
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## API key
|
|
48
|
-
|
|
49
|
-
messygit resolves the key in this order:
|
|
50
|
-
|
|
51
|
-
1. Environment variable **`ANTHROPIC_API_KEY`**
|
|
52
|
-
2. Config file **`~/.messygit/config.json`** (written by `messygit config`)
|
|
53
|
-
|
|
54
|
-
If neither is set, the default command exits with a short message explaining how to fix it.
|
|
55
|
-
|
|
56
|
-
**Save a key to the config file:**
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
messygit config --key YOUR_ANTHROPIC_API_KEY
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Show a masked key** (which source is active, without revealing the secret):
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
messygit show
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Usage
|
|
69
|
-
|
|
70
|
-
Typical flow:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
git add .
|
|
74
|
-
messygit
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
1. If there is nothing staged, messygit tells you to run `git add` first.
|
|
78
|
-
2. Otherwise it sends the staged diff to Claude and prints a suggested one-line message.
|
|
79
|
-
3. You are prompted: **commit** (default), **no** (cancel), or **edit** (open `$EDITOR` to change the message).
|
|
80
|
-
4. On confirmation, it runs `git commit -m "..."` with your chosen text.
|
|
81
|
-
|
|
82
|
-
### Commands
|
|
83
|
-
|
|
84
|
-
| Command | Description |
|
|
85
|
-
|--------|-------------|
|
|
86
|
-
| `messygit` | Generate a message from `git diff --staged`, then prompt to commit / cancel / edit. |
|
|
87
|
-
| `messygit config --key KEY` | Store the Anthropic API key under `~/.messygit/config.json`. |
|
|
88
|
-
| `messygit show` | Print a masked API key and whether it comes from the environment or config file. |
|
|
89
|
-
|
|
90
|
-
### Commit message style
|
|
91
|
-
|
|
92
|
-
The model is instructed to follow **Conventional Commits**, for example:
|
|
93
|
-
|
|
94
|
-
`feat(auth): validate refresh tokens`
|
|
95
|
-
|
|
96
|
-
Allowed types include: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Subjects are one line, imperative, lowercase, no trailing period, and kept within a reasonable length (see your prompts in the package if you customize behavior).
|
|
97
|
-
|
|
98
|
-
## Development
|
|
99
|
-
|
|
100
|
-
Without installing the package, from the **repository root** (the directory that contains the `messygit` package folder):
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
.venv/bin/python -m messygit.cli
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Subcommands:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
python -m messygit.cli config --key YOUR_KEY
|
|
110
|
-
python -m messygit.cli show
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## License
|
|
114
|
-
|
|
115
|
-
MIT (see `pyproject.toml`).
|
messygit-0.1.4/README.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# messygit
|
|
2
|
-
|
|
3
|
-
**messygit** is a command-line tool that reads your **staged** Git changes, asks **Claude** (via the [Anthropic API](https://www.anthropic.com/api)) to suggest a **Conventional Commits** subject line, and then lets you **commit**, **cancel**, or **edit** the message before running `git commit`.
|
|
4
|
-
|
|
5
|
-
## Why use it
|
|
6
|
-
|
|
7
|
-
- Keeps commit subjects consistent (`feat(scope): describe the change`) without thinking up wording from scratch.
|
|
8
|
-
- Only the **staged** diff is sent to the model—what you `git add` is what gets summarized.
|
|
9
|
-
- The API key is never printed in full; `show` uses a masked preview.
|
|
10
|
-
- Clear errors for missing keys, rejected keys, and billing or zero-balance situations.
|
|
11
|
-
|
|
12
|
-
## Requirements
|
|
13
|
-
|
|
14
|
-
- **Python** 3.10 or newer
|
|
15
|
-
- **Git** (run inside a repository)
|
|
16
|
-
- An **Anthropic API key** with access to the Messages API
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install messygit
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
This installs the `messygit` command (see `[project.scripts]` in `pyproject.toml`).
|
|
25
|
-
|
|
26
|
-
### Install from source
|
|
27
|
-
|
|
28
|
-
From a checkout of this project:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
cd messygit
|
|
32
|
-
python -m venv .venv
|
|
33
|
-
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
34
|
-
pip install -e .
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## API key
|
|
38
|
-
|
|
39
|
-
messygit resolves the key in this order:
|
|
40
|
-
|
|
41
|
-
1. Environment variable **`ANTHROPIC_API_KEY`**
|
|
42
|
-
2. Config file **`~/.messygit/config.json`** (written by `messygit config`)
|
|
43
|
-
|
|
44
|
-
If neither is set, the default command exits with a short message explaining how to fix it.
|
|
45
|
-
|
|
46
|
-
**Save a key to the config file:**
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
messygit config --key YOUR_ANTHROPIC_API_KEY
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**Show a masked key** (which source is active, without revealing the secret):
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
messygit show
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Usage
|
|
59
|
-
|
|
60
|
-
Typical flow:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
git add .
|
|
64
|
-
messygit
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
1. If there is nothing staged, messygit tells you to run `git add` first.
|
|
68
|
-
2. Otherwise it sends the staged diff to Claude and prints a suggested one-line message.
|
|
69
|
-
3. You are prompted: **commit** (default), **no** (cancel), or **edit** (open `$EDITOR` to change the message).
|
|
70
|
-
4. On confirmation, it runs `git commit -m "..."` with your chosen text.
|
|
71
|
-
|
|
72
|
-
### Commands
|
|
73
|
-
|
|
74
|
-
| Command | Description |
|
|
75
|
-
|--------|-------------|
|
|
76
|
-
| `messygit` | Generate a message from `git diff --staged`, then prompt to commit / cancel / edit. |
|
|
77
|
-
| `messygit config --key KEY` | Store the Anthropic API key under `~/.messygit/config.json`. |
|
|
78
|
-
| `messygit show` | Print a masked API key and whether it comes from the environment or config file. |
|
|
79
|
-
|
|
80
|
-
### Commit message style
|
|
81
|
-
|
|
82
|
-
The model is instructed to follow **Conventional Commits**, for example:
|
|
83
|
-
|
|
84
|
-
`feat(auth): validate refresh tokens`
|
|
85
|
-
|
|
86
|
-
Allowed types include: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Subjects are one line, imperative, lowercase, no trailing period, and kept within a reasonable length (see your prompts in the package if you customize behavior).
|
|
87
|
-
|
|
88
|
-
## Development
|
|
89
|
-
|
|
90
|
-
Without installing the package, from the **repository root** (the directory that contains the `messygit` package folder):
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
.venv/bin/python -m messygit.cli
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Subcommands:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
python -m messygit.cli config --key YOUR_KEY
|
|
100
|
-
python -m messygit.cli show
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## License
|
|
104
|
-
|
|
105
|
-
MIT (see `pyproject.toml`).
|
messygit-0.1.4/messygit/cli.py
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
import click
|
|
4
|
-
|
|
5
|
-
from .config import (
|
|
6
|
-
ANTHROPIC_ENV_VAR,
|
|
7
|
-
CONFIG_FILE,
|
|
8
|
-
AnthropicInsufficientBalanceError,
|
|
9
|
-
InvalidAnthropicCredentialsError,
|
|
10
|
-
MissingApiKeyError,
|
|
11
|
-
load_api_key,
|
|
12
|
-
mask_api_key,
|
|
13
|
-
save_api_key,
|
|
14
|
-
)
|
|
15
|
-
from .git import get_staged_diff, git_commit
|
|
16
|
-
from .llm import generate_commit_message
|
|
17
|
-
|
|
18
|
-
def _prompt_commit_action(message: str) -> None:
|
|
19
|
-
"""Ask [Y/n/e]: commit, cancel, or edit in $EDITOR."""
|
|
20
|
-
current = message
|
|
21
|
-
while True:
|
|
22
|
-
click.echo(current)
|
|
23
|
-
choice = click.prompt(
|
|
24
|
-
"Commit with this message? [y/n/e]",
|
|
25
|
-
default="Y",
|
|
26
|
-
show_default=False,
|
|
27
|
-
).strip().lower()
|
|
28
|
-
|
|
29
|
-
if choice in ("", "y", "yes"):
|
|
30
|
-
result = git_commit(current)
|
|
31
|
-
if result.stdout:
|
|
32
|
-
click.echo(result.stdout, nl=False)
|
|
33
|
-
if result.stderr:
|
|
34
|
-
click.echo(result.stderr, nl=False, err=True)
|
|
35
|
-
if result.returncode != 0:
|
|
36
|
-
raise click.ClickException("git commit failed.")
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
if choice in ("n", "no"):
|
|
40
|
-
click.echo("Commit cancelled.")
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
if choice in ("e", "edit"):
|
|
44
|
-
edited = click.edit(current)
|
|
45
|
-
if edited is None:
|
|
46
|
-
click.echo("Editor exited without saving; message unchanged.")
|
|
47
|
-
continue
|
|
48
|
-
stripped = edited.strip()
|
|
49
|
-
if not stripped:
|
|
50
|
-
click.echo("Empty message ignored; message unchanged.")
|
|
51
|
-
continue
|
|
52
|
-
current = stripped
|
|
53
|
-
continue
|
|
54
|
-
|
|
55
|
-
click.echo("Please answer y (yes), n (no), or e (edit).")
|
|
56
|
-
|
|
57
|
-
@click.group(invoke_without_command=True)
|
|
58
|
-
@click.pass_context
|
|
59
|
-
def main(ctx):
|
|
60
|
-
"""Messy Git is a tool that analyzes your updated code and generates clean commit messages. Let's keep your "messy" messages in check!"""
|
|
61
|
-
if ctx.invoked_subcommand is None:
|
|
62
|
-
diff = get_staged_diff()
|
|
63
|
-
if not diff.strip():
|
|
64
|
-
raise click.ClickException(
|
|
65
|
-
"No staged changes found. Run 'git add' first."
|
|
66
|
-
)
|
|
67
|
-
try:
|
|
68
|
-
message = generate_commit_message(diff)
|
|
69
|
-
except MissingApiKeyError as e:
|
|
70
|
-
raise click.ClickException(str(e)) from e
|
|
71
|
-
except InvalidAnthropicCredentialsError as e:
|
|
72
|
-
raise click.ClickException(str(e)) from e
|
|
73
|
-
except AnthropicInsufficientBalanceError as e:
|
|
74
|
-
raise click.ClickException(str(e)) from e
|
|
75
|
-
_prompt_commit_action(message)
|
|
76
|
-
|
|
77
|
-
@main.command("config")
|
|
78
|
-
@click.option("--key", type=str, required=True, help="Anthropic API key")
|
|
79
|
-
def config_cmd(key):
|
|
80
|
-
"""Configure your Anthropic API key."""
|
|
81
|
-
try:
|
|
82
|
-
save_api_key(key)
|
|
83
|
-
except ValueError as e:
|
|
84
|
-
raise click.ClickException(str(e)) from e
|
|
85
|
-
click.echo(f"API key saved successfully ({mask_api_key(key.strip())})")
|
|
86
|
-
|
|
87
|
-
@main.command("show")
|
|
88
|
-
def show():
|
|
89
|
-
"""Display masked API key (env takes precedence over config file)."""
|
|
90
|
-
env_set = ANTHROPIC_ENV_VAR in os.environ
|
|
91
|
-
env_key = (os.environ.get(ANTHROPIC_ENV_VAR) or "").strip()
|
|
92
|
-
if env_key:
|
|
93
|
-
click.echo(f"API key: {mask_api_key(env_key)} (from ANTHROPIC_API_KEY)")
|
|
94
|
-
return
|
|
95
|
-
file_key = load_api_key()
|
|
96
|
-
if file_key:
|
|
97
|
-
if env_set:
|
|
98
|
-
click.echo(
|
|
99
|
-
f"{ANTHROPIC_ENV_VAR} is set but empty; showing key from {CONFIG_FILE}."
|
|
100
|
-
)
|
|
101
|
-
click.echo(f"API key: {mask_api_key(file_key)} (from {CONFIG_FILE})")
|
|
102
|
-
return
|
|
103
|
-
if env_set:
|
|
104
|
-
click.echo(
|
|
105
|
-
f"{ANTHROPIC_ENV_VAR} is set but empty or whitespace-only, and no usable key "
|
|
106
|
-
f"is stored in {CONFIG_FILE}. Unset the variable or run messygit config --key."
|
|
107
|
-
)
|
|
108
|
-
return
|
|
109
|
-
click.echo("No API key found. Set ANTHROPIC_API_KEY or run messygit config --key.")
|
|
110
|
-
|
|
111
|
-
if __name__ == "__main__":
|
|
112
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|