agent-gitv1 0.1.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.
PKG-INFO ADDED
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-gitv1
3
+ Version: 0.1.0
4
+ Summary: AI-powered Git CLI using MCP + Gemini to auto-generate commit messages
5
+ Author-email: Vijay <you@example.com>
6
+ License: MIT
7
+ Keywords: ai,automation,cli,gemini,git,mcp
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: google-genai>=0.8.0
11
+ Requires-Dist: mcp[cli]>=1.0.0
12
+ Requires-Dist: openai>=1.14.0
13
+ Requires-Dist: requests>=2.31.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # agent-gitv1 πŸ€–
17
+
18
+ > **AI-powered Git CLI** β€” Uses [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) + Google Gemini to automatically generate professional commit messages from your diff.
19
+
20
+ ---
21
+
22
+ ## How It Works
23
+
24
+ ```
25
+ Your Code Changes
26
+ β”‚
27
+ β–Ό
28
+ [MCP Client] ──stdio──► [mcp-server-git]
29
+ β”‚ β”‚
30
+ │◄── git diff β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
31
+ β”‚
32
+ β–Ό
33
+ [Gemini AI] ──► Generate commit message
34
+ β”‚
35
+ β–Ό
36
+ [MCP Client] ──► git add . ──► git commit
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Prerequisites
42
+
43
+ | Tool | Install |
44
+ |------|---------|
45
+ | Python β‰₯ 3.10 | [python.org](https://python.org) |
46
+ | `uvx` (uv tool runner) | `pip install uv` |
47
+ | `mcp-server-git` | Auto-fetched by `uvx` |
48
+ | Gemini API Key | [aistudio.google.com](https://aistudio.google.com) |
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ # 1. Clone / navigate to the project
56
+ cd "d:\Vijay Projects\Agent_bhai"
57
+
58
+ # 2. Install in editable mode (creates the `agent` command globally)
59
+ pip install -e .
60
+
61
+ # 3. Set your Gemini API key
62
+ set GEMINI_API_KEY=your_gemini_api_key_here # Windows CMD
63
+ $env:GEMINI_API_KEY="your_key" # Windows PowerShell
64
+ export GEMINI_API_KEY=your_gemini_api_key_here # Linux / Mac
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Usage
70
+
71
+ ### `agent config` β€” Configure LLM Provider (Start Here!)
72
+
73
+ Run this to configure OpenAI, Google Gemini, or Ollama.
74
+
75
+ ```bash
76
+ agent config
77
+ ```
78
+
79
+ What you can configure:
80
+ - **Google Gemini**: Uses your API key and a model like `gemini-2.0-flash`.
81
+ - **OpenAI (ChatGPT)**: Uses your API key and a model like `gpt-4o-mini`.
82
+ - **Ollama (Local/Network)**: Provide the base URL (e.g. `http://localhost:11434` or `http://192.168.1.50:11434`). The CLI will automatically fetch your downloaded models and let you choose one!
83
+
84
+ ---
85
+
86
+ ### `agent commit` β€” Stage + AI commit message + commit
87
+
88
+ ```bash
89
+ # In any git repo:
90
+ agent commit
91
+
92
+ # Specify a repo path:
93
+ agent commit --repo /path/to/repo
94
+
95
+ # Generate multiple suggestions (pick one or type your own):
96
+ agent commit --suggestions 3
97
+
98
+ # Verbose mode (shows diff preview + available MCP tools):
99
+ agent commit --verbose
100
+ agent commit -v
101
+ ```
102
+
103
+ `agent commit` is now history-aware:
104
+ - It uses recent commit messages from your repo to align tone/style.
105
+ - It includes changed file names in the LLM prompt for better scoped messages.
106
+
107
+ ### `agent push` β€” Push to remote
108
+
109
+ ```bash
110
+ agent push
111
+ agent push --remote origin --branch main
112
+ ```
113
+
114
+ ### Help
115
+
116
+ ```bash
117
+ agent --help
118
+ agent config --help
119
+ agent commit --help
120
+ agent push --help
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Example Session
126
+
127
+ ```
128
+ πŸ—‚ Repository: D:\my-project
129
+
130
+ πŸ”Œ Connecting to mcp-server-git...
131
+ βœ… MCP session initialized.
132
+
133
+ πŸ“‚ Fetching git diff (unstaged changes)...
134
+ Diff captured (1240 chars).
135
+
136
+ πŸ€– Generating commit message with Gemini...
137
+
138
+ πŸ’¬ Commit Message: feat(auth): add JWT token refresh endpoint
139
+
140
+ Proceed with git add + commit? [Y/n]: y
141
+
142
+ πŸ“¦ Staging all changes (git add .)...
143
+ Files staged.
144
+
145
+ ✍️ Committing...
146
+ [main a3f12bc] feat(auth): add JWT token refresh endpoint
147
+
148
+ πŸš€ Done! Changes committed successfully.
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Environment Variables
154
+
155
+ | Variable | Required | Description |
156
+ |----------|----------|-------------|
157
+ | `GEMINI_API_KEY` | βœ… Yes | Your Google Gemini API key |
158
+
159
+ ---
160
+
161
+ ## Project Structure
162
+
163
+ ```
164
+ Agent_bhai/
165
+ β”œβ”€β”€ agent.py # Main CLI + MCP client logic
166
+ β”œβ”€β”€ pyproject.toml # Packaging + entry point config
167
+ └── README.md # This file
168
+ ```
169
+
170
+ ---
171
+
172
+ ## License
173
+
174
+ MIT
README.md ADDED
@@ -0,0 +1,159 @@
1
+ # agent-gitv1 πŸ€–
2
+
3
+ > **AI-powered Git CLI** β€” Uses [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) + Google Gemini to automatically generate professional commit messages from your diff.
4
+
5
+ ---
6
+
7
+ ## How It Works
8
+
9
+ ```
10
+ Your Code Changes
11
+ β”‚
12
+ β–Ό
13
+ [MCP Client] ──stdio──► [mcp-server-git]
14
+ β”‚ β”‚
15
+ │◄── git diff β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
16
+ β”‚
17
+ β–Ό
18
+ [Gemini AI] ──► Generate commit message
19
+ β”‚
20
+ β–Ό
21
+ [MCP Client] ──► git add . ──► git commit
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Prerequisites
27
+
28
+ | Tool | Install |
29
+ |------|---------|
30
+ | Python β‰₯ 3.10 | [python.org](https://python.org) |
31
+ | `uvx` (uv tool runner) | `pip install uv` |
32
+ | `mcp-server-git` | Auto-fetched by `uvx` |
33
+ | Gemini API Key | [aistudio.google.com](https://aistudio.google.com) |
34
+
35
+ ---
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ # 1. Clone / navigate to the project
41
+ cd "d:\Vijay Projects\Agent_bhai"
42
+
43
+ # 2. Install in editable mode (creates the `agent` command globally)
44
+ pip install -e .
45
+
46
+ # 3. Set your Gemini API key
47
+ set GEMINI_API_KEY=your_gemini_api_key_here # Windows CMD
48
+ $env:GEMINI_API_KEY="your_key" # Windows PowerShell
49
+ export GEMINI_API_KEY=your_gemini_api_key_here # Linux / Mac
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Usage
55
+
56
+ ### `agent config` β€” Configure LLM Provider (Start Here!)
57
+
58
+ Run this to configure OpenAI, Google Gemini, or Ollama.
59
+
60
+ ```bash
61
+ agent config
62
+ ```
63
+
64
+ What you can configure:
65
+ - **Google Gemini**: Uses your API key and a model like `gemini-2.0-flash`.
66
+ - **OpenAI (ChatGPT)**: Uses your API key and a model like `gpt-4o-mini`.
67
+ - **Ollama (Local/Network)**: Provide the base URL (e.g. `http://localhost:11434` or `http://192.168.1.50:11434`). The CLI will automatically fetch your downloaded models and let you choose one!
68
+
69
+ ---
70
+
71
+ ### `agent commit` β€” Stage + AI commit message + commit
72
+
73
+ ```bash
74
+ # In any git repo:
75
+ agent commit
76
+
77
+ # Specify a repo path:
78
+ agent commit --repo /path/to/repo
79
+
80
+ # Generate multiple suggestions (pick one or type your own):
81
+ agent commit --suggestions 3
82
+
83
+ # Verbose mode (shows diff preview + available MCP tools):
84
+ agent commit --verbose
85
+ agent commit -v
86
+ ```
87
+
88
+ `agent commit` is now history-aware:
89
+ - It uses recent commit messages from your repo to align tone/style.
90
+ - It includes changed file names in the LLM prompt for better scoped messages.
91
+
92
+ ### `agent push` β€” Push to remote
93
+
94
+ ```bash
95
+ agent push
96
+ agent push --remote origin --branch main
97
+ ```
98
+
99
+ ### Help
100
+
101
+ ```bash
102
+ agent --help
103
+ agent config --help
104
+ agent commit --help
105
+ agent push --help
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Example Session
111
+
112
+ ```
113
+ πŸ—‚ Repository: D:\my-project
114
+
115
+ πŸ”Œ Connecting to mcp-server-git...
116
+ βœ… MCP session initialized.
117
+
118
+ πŸ“‚ Fetching git diff (unstaged changes)...
119
+ Diff captured (1240 chars).
120
+
121
+ πŸ€– Generating commit message with Gemini...
122
+
123
+ πŸ’¬ Commit Message: feat(auth): add JWT token refresh endpoint
124
+
125
+ Proceed with git add + commit? [Y/n]: y
126
+
127
+ πŸ“¦ Staging all changes (git add .)...
128
+ Files staged.
129
+
130
+ ✍️ Committing...
131
+ [main a3f12bc] feat(auth): add JWT token refresh endpoint
132
+
133
+ πŸš€ Done! Changes committed successfully.
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Environment Variables
139
+
140
+ | Variable | Required | Description |
141
+ |----------|----------|-------------|
142
+ | `GEMINI_API_KEY` | βœ… Yes | Your Google Gemini API key |
143
+
144
+ ---
145
+
146
+ ## Project Structure
147
+
148
+ ```
149
+ Agent_bhai/
150
+ β”œβ”€β”€ agent.py # Main CLI + MCP client logic
151
+ β”œβ”€β”€ pyproject.toml # Packaging + entry point config
152
+ └── README.md # This file
153
+ ```
154
+
155
+ ---
156
+
157
+ ## License
158
+
159
+ MIT
agent.py ADDED
@@ -0,0 +1,716 @@
1
+ """
2
+ agent-gitv1: A CLI tool that uses MCP + Local/Cloud LLMs to automate git workflows.
3
+ """
4
+
5
+ import asyncio
6
+ import sys
7
+ import os
8
+ import json
9
+ import re
10
+ import subprocess
11
+ from pathlib import Path
12
+ import click
13
+ from mcp import ClientSession, StdioServerParameters
14
+ from mcp.client.stdio import stdio_client
15
+
16
+
17
+ # ─────────────────────────────────────────────
18
+ # Configuration
19
+ # ─────────────────────────────────────────────
20
+
21
+ CONFIG_FILE = Path.home() / ".agent_git_config.json"
22
+ DEFAULT_DIFF_TARGET = "HEAD"
23
+ MAX_DIFF_CHARS_FOR_LLM = 28000
24
+ MAX_FILES_FOR_LLM_DIFF = 40
25
+ MAX_CHARS_PER_FILE_SECTION = 1400
26
+ MAX_HISTORY_COMMITS = 12
27
+ MAX_HISTORY_CHARS = 2400
28
+
29
+ COMMIT_SYSTEM_PROMPT = """
30
+ You are an expert software engineer. Your ONLY job is to write a concise, professional Git commit message.
31
+
32
+ Rules:
33
+ - Use the Conventional Commits format: <type>(<optional scope>): <short summary>
34
+ - Types: feat, fix, docs, style, refactor, test, chore, ci, perf
35
+ - Summary must be in imperative mood (e.g., "add feature" not "added feature")
36
+ - Keep the summary under 72 characters
37
+ - Do NOT include explanations, markdown, backticks, or any extra text
38
+ - Output ONLY the commit message, nothing else
39
+
40
+ Examples:
41
+ feat(auth): add JWT token refresh endpoint
42
+ fix(ui): resolve button alignment on mobile devices
43
+ docs: update README with installation steps
44
+ refactor(api): simplify error handling middleware
45
+ """
46
+
47
+ COMMIT_TYPE_PATTERN = r"(?:feat|fix|docs|style|refactor|test|chore|ci|perf)"
48
+ COMMIT_MESSAGE_PATTERN = re.compile(
49
+ rf"^{COMMIT_TYPE_PATTERN}(?:\([^)]+\))?: .+"
50
+ )
51
+
52
+ def load_config() -> dict:
53
+ if CONFIG_FILE.exists():
54
+ try:
55
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
56
+ return json.load(f)
57
+ except Exception:
58
+ return {}
59
+ return {}
60
+
61
+ def save_config(config: dict):
62
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
63
+ json.dump(config, f, indent=2)
64
+
65
+
66
+ # ─────────────────────────────────────────────
67
+ # AI Providers
68
+ # ─────────────────────────────────────────────
69
+
70
+ def get_ollama_models(base_url: str):
71
+ import requests
72
+ try:
73
+ url = base_url.rstrip("/") + "/api/tags"
74
+ resp = requests.get(url, timeout=3)
75
+ resp.raise_for_status()
76
+ return [m["name"] for m in resp.json().get("models", [])]
77
+ except Exception:
78
+ return []
79
+
80
+ def generate_with_gemini(diff: str, config: dict) -> str:
81
+ from google import genai
82
+ from google.genai import types
83
+ api_key = config.get("api_key") or os.environ.get("GEMINI_API_KEY")
84
+ if not api_key:
85
+ raise click.ClickException("Gemini API key is not configured. Run 'agent config' or set GEMINI_API_KEY.")
86
+
87
+ client = genai.Client(api_key=api_key)
88
+ user_prompt = diff
89
+ response = client.models.generate_content(
90
+ model=config.get("model", "gemini-2.0-flash"),
91
+ contents=user_prompt,
92
+ config=types.GenerateContentConfig(
93
+ system_instruction=COMMIT_SYSTEM_PROMPT,
94
+ temperature=0.2,
95
+ ),
96
+ )
97
+ return response.text.strip()
98
+
99
+ def generate_with_openai(diff: str, config: dict) -> str:
100
+ from openai import OpenAI
101
+ api_key = config.get("api_key") or os.environ.get("OPENAI_API_KEY")
102
+ if not api_key:
103
+ raise click.ClickException("OpenAI API key is not configured. Run 'agent config' or set OPENAI_API_KEY.")
104
+
105
+ client = OpenAI(api_key=api_key)
106
+ response = client.chat.completions.create(
107
+ model=config.get("model", "gpt-4o-mini"),
108
+ messages=[
109
+ {"role": "system", "content": COMMIT_SYSTEM_PROMPT},
110
+ {"role": "user", "content": diff}
111
+ ],
112
+ temperature=0.2,
113
+ )
114
+ return response.choices[0].message.content.strip()
115
+
116
+ def generate_with_ollama(diff: str, config: dict) -> str:
117
+ from openai import OpenAI
118
+ base_url = config.get("base_url", "http://localhost:11434")
119
+ # Ollama uses the OpenAI client but needs the /v1 suffix if not provided
120
+ openai_base_url = base_url.rstrip("/") + "/v1"
121
+
122
+ # Fake API key for Ollama compatibility
123
+ client = OpenAI(base_url=openai_base_url, api_key="ollama-local")
124
+ try:
125
+ response = client.chat.completions.create(
126
+ model=config.get("model", "llama3:latest"),
127
+ messages=[
128
+ {"role": "system", "content": COMMIT_SYSTEM_PROMPT},
129
+ {"role": "user", "content": diff}
130
+ ],
131
+ temperature=0.2,
132
+ )
133
+ return response.choices[0].message.content.strip()
134
+ except Exception as e:
135
+ raise click.ClickException(f"Ollama generation failed: {e}\nIs Ollama running at {base_url}?")
136
+
137
+ def generate_commit_message(diff: str) -> str:
138
+ """Send the git diff to the configured AI and get back a commit message."""
139
+ config = load_config()
140
+ provider = config.get("provider", "gemini") # Default fallback
141
+
142
+ if provider == "gemini":
143
+ return generate_with_gemini(diff, config)
144
+ elif provider == "openai":
145
+ return generate_with_openai(diff, config)
146
+ elif provider == "ollama":
147
+ return generate_with_ollama(diff, config)
148
+ else:
149
+ raise click.ClickException(f"Unknown provider '{provider}'. Run 'agent config'.")
150
+
151
+ def build_commit_system_prompt(suggestions: int) -> str:
152
+ if suggestions <= 1:
153
+ return COMMIT_SYSTEM_PROMPT
154
+
155
+ return f"""
156
+ You are an expert software engineer. Your ONLY job is to write concise, professional Git commit messages.
157
+
158
+ Rules:
159
+ - Use the Conventional Commits format: <type>(<optional scope>): <short summary>
160
+ - Types: feat, fix, docs, style, refactor, test, chore, ci, perf
161
+ - Summary must be in imperative mood (e.g., "add feature" not "added feature")
162
+ - Keep each summary under 72 characters
163
+ - Return exactly {suggestions} options
164
+ - Each option must be on a new line with numbering: "1. ...", "2. ..."
165
+ - Do NOT include explanations, markdown, backticks, or any extra text
166
+ """
167
+
168
+ def extract_changed_files(diff_text: str, max_files: int = 20) -> list[str]:
169
+ files = []
170
+ for line in diff_text.splitlines():
171
+ if not line.startswith("diff --git "):
172
+ continue
173
+ parts = line.split()
174
+ if len(parts) >= 4:
175
+ path = parts[3]
176
+ if path.startswith("b/"):
177
+ path = path[2:]
178
+ if path not in files:
179
+ files.append(path)
180
+ if len(files) >= max_files:
181
+ break
182
+ return files
183
+
184
+ def get_recent_commit_history(repo_path: str, max_commits: int = MAX_HISTORY_COMMITS) -> str:
185
+ try:
186
+ cmd = [
187
+ "git",
188
+ "log",
189
+ f"-n{max_commits}",
190
+ "--pretty=format:%h %s",
191
+ ]
192
+ output = subprocess.check_output(
193
+ cmd,
194
+ cwd=repo_path,
195
+ text=True,
196
+ stderr=subprocess.DEVNULL,
197
+ ).strip()
198
+ except Exception:
199
+ return ""
200
+
201
+ if not output:
202
+ return ""
203
+
204
+ if len(output) > MAX_HISTORY_CHARS:
205
+ return output[:MAX_HISTORY_CHARS] + "\n... [history truncated]"
206
+ return output
207
+
208
+ def build_commit_user_prompt(
209
+ diff_text: str,
210
+ history_text: str,
211
+ changed_files: list[str],
212
+ ) -> str:
213
+ files_block = "\n".join(f"- {name}" for name in changed_files) if changed_files else "- (not detected)"
214
+ history_block = history_text if history_text else "(No recent commits available)"
215
+ return (
216
+ "Write commit message suggestion(s) for the following change.\n\n"
217
+ "Recent commit messages from this repository (for style/context):\n"
218
+ f"{history_block}\n\n"
219
+ "Changed files:\n"
220
+ f"{files_block}\n\n"
221
+ "Git diff:\n"
222
+ f"{diff_text}"
223
+ )
224
+
225
+ def sanitize_commit_candidate(text: str) -> str:
226
+ cleaned = text.strip().strip("`").strip("\"").strip("'")
227
+ cleaned = re.sub(r"^\d+\s*[\.\)\-:]\s*", "", cleaned).strip()
228
+ return cleaned
229
+
230
+ def parse_commit_suggestions(raw_text: str, expected: int) -> list[str]:
231
+ suggestions = []
232
+ for line in raw_text.splitlines():
233
+ candidate = sanitize_commit_candidate(line)
234
+ if COMMIT_MESSAGE_PATTERN.match(candidate):
235
+ if candidate not in suggestions:
236
+ suggestions.append(candidate)
237
+ if len(suggestions) >= expected:
238
+ return suggestions
239
+
240
+ matches = re.findall(
241
+ rf"(?:^|\n)\s*(?:\d+\s*[\.\)\-:]\s*)?({COMMIT_TYPE_PATTERN}(?:\([^)]+\))?: [^\n]+)",
242
+ raw_text,
243
+ flags=re.IGNORECASE,
244
+ )
245
+ for match in matches:
246
+ candidate = sanitize_commit_candidate(match)
247
+ if COMMIT_MESSAGE_PATTERN.match(candidate) and candidate not in suggestions:
248
+ suggestions.append(candidate)
249
+ if len(suggestions) >= expected:
250
+ return suggestions
251
+
252
+ # Fallback: take first non-empty line even if model ignored format rules.
253
+ if not suggestions:
254
+ for line in raw_text.splitlines():
255
+ candidate = sanitize_commit_candidate(line)
256
+ if candidate:
257
+ suggestions.append(candidate)
258
+ break
259
+ return suggestions
260
+
261
+ def generate_commit_suggestions(
262
+ diff_text: str,
263
+ repo_path: str,
264
+ suggestions: int,
265
+ ) -> list[str]:
266
+ config = load_config()
267
+ provider = config.get("provider", "gemini") # Default fallback
268
+
269
+ history_text = get_recent_commit_history(repo_path)
270
+ changed_files = extract_changed_files(diff_text)
271
+ user_prompt = build_commit_user_prompt(diff_text, history_text, changed_files)
272
+ system_prompt = build_commit_system_prompt(suggestions)
273
+
274
+ if provider == "gemini":
275
+ from google import genai
276
+ from google.genai import types
277
+ api_key = config.get("api_key") or os.environ.get("GEMINI_API_KEY")
278
+ if not api_key:
279
+ raise click.ClickException("Gemini API key is not configured. Run 'agent config' or set GEMINI_API_KEY.")
280
+ client = genai.Client(api_key=api_key)
281
+ response = client.models.generate_content(
282
+ model=config.get("model", "gemini-2.0-flash"),
283
+ contents=user_prompt,
284
+ config=types.GenerateContentConfig(
285
+ system_instruction=system_prompt,
286
+ temperature=0.2,
287
+ ),
288
+ )
289
+ raw_text = (response.text or "").strip()
290
+ elif provider in ("openai", "ollama"):
291
+ from openai import OpenAI
292
+ if provider == "openai":
293
+ api_key = config.get("api_key") or os.environ.get("OPENAI_API_KEY")
294
+ if not api_key:
295
+ raise click.ClickException("OpenAI API key is not configured. Run 'agent config' or set OPENAI_API_KEY.")
296
+ client = OpenAI(api_key=api_key)
297
+ model = config.get("model", "gpt-4o-mini")
298
+ else:
299
+ base_url = config.get("base_url", "http://localhost:11434").rstrip("/") + "/v1"
300
+ client = OpenAI(base_url=base_url, api_key="ollama-local")
301
+ model = config.get("model", "llama3:latest")
302
+
303
+ try:
304
+ response = client.chat.completions.create(
305
+ model=model,
306
+ messages=[
307
+ {"role": "system", "content": system_prompt},
308
+ {"role": "user", "content": user_prompt},
309
+ ],
310
+ temperature=0.2,
311
+ )
312
+ except Exception as e:
313
+ if provider == "ollama":
314
+ raise click.ClickException(f"Ollama generation failed: {e}\nIs Ollama running at {config.get('base_url', 'http://localhost:11434')}?")
315
+ raise
316
+ raw_text = (response.choices[0].message.content or "").strip()
317
+ else:
318
+ raise click.ClickException(f"Unknown provider '{provider}'. Run 'agent config'.")
319
+
320
+ parsed = parse_commit_suggestions(raw_text, suggestions)
321
+ if not parsed:
322
+ raise click.ClickException("Model returned no usable commit message suggestions.")
323
+ return parsed
324
+
325
+ def extract_tool_text(result) -> str:
326
+ """Collect text blocks from an MCP tool response."""
327
+ output = ""
328
+ for block in result.content:
329
+ if hasattr(block, "text"):
330
+ output += block.text
331
+ return output.strip()
332
+
333
+ def split_diff_sections(diff_text: str):
334
+ """
335
+ Split unified diff into per-file sections starting at 'diff --git'.
336
+ If no markers exist, return the original text as one section.
337
+ """
338
+ lines = diff_text.splitlines(keepends=True)
339
+ sections = []
340
+ current = []
341
+
342
+ for line in lines:
343
+ if line.startswith("diff --git "):
344
+ if current:
345
+ sections.append("".join(current))
346
+ current = [line]
347
+ else:
348
+ current.append(line)
349
+
350
+ if current:
351
+ sections.append("".join(current))
352
+
353
+ if not sections:
354
+ return [diff_text]
355
+ return sections
356
+
357
+ def build_llm_diff_payload(diff_text: str):
358
+ """
359
+ Keep prompt size bounded for better LLM latency/quality on very large diffs.
360
+ Returns (payload_text, was_truncated).
361
+ """
362
+ if len(diff_text) <= MAX_DIFF_CHARS_FOR_LLM:
363
+ return diff_text, False
364
+
365
+ sections = split_diff_sections(diff_text)
366
+ compact_sections = []
367
+ total_files = len(sections)
368
+
369
+ for idx, section in enumerate(sections):
370
+ if idx >= MAX_FILES_FOR_LLM_DIFF:
371
+ break
372
+ if len(section) > MAX_CHARS_PER_FILE_SECTION:
373
+ compact_sections.append(
374
+ section[:MAX_CHARS_PER_FILE_SECTION]
375
+ + f"\n... [section truncated at {MAX_CHARS_PER_FILE_SECTION} chars]\n"
376
+ )
377
+ else:
378
+ compact_sections.append(section)
379
+
380
+ remaining_files = max(0, total_files - len(compact_sections))
381
+ header = (
382
+ f"[Truncated diff for AI input: original={len(diff_text)} chars, "
383
+ f"included_files={len(compact_sections)}, omitted_files={remaining_files}]\n\n"
384
+ )
385
+ payload = header + "".join(compact_sections)
386
+
387
+ if len(payload) > MAX_DIFF_CHARS_FOR_LLM:
388
+ payload = payload[:MAX_DIFF_CHARS_FOR_LLM] + "\n... [overall diff payload truncated]\n"
389
+
390
+ return payload, True
391
+
392
+
393
+ # ─────────────────────────────────────────────
394
+ # MCP Core Logic
395
+ # ─────────────────────────────────────────────
396
+
397
+ async def run_agent_commit(repo_path: str, verbose: bool, suggestion_count: int):
398
+ """
399
+ Full MCP lifecycle:
400
+ 1. Initialize MCP session with mcp-server-git
401
+ 2. Call git_diff to get uncommitted changes
402
+ 3. Generate commit message via selected AI Provider
403
+ 4. Call git_add then git_commit via MCP
404
+ """
405
+ server_params = StdioServerParameters(
406
+ command="uvx",
407
+ args=["mcp-server-git", "--repository", repo_path],
408
+ env=None,
409
+ )
410
+
411
+ click.echo(click.style("πŸ”Œ Connecting to mcp-server-git...", fg="cyan"))
412
+
413
+ async with stdio_client(server_params) as (read, write):
414
+ async with ClientSession(read, write) as session:
415
+
416
+ # ── Step 1: Initialize ──────────────────────────
417
+ await session.initialize()
418
+ click.echo(click.style("βœ… MCP session initialized.", fg="green"))
419
+
420
+ if verbose:
421
+ tools = await session.list_tools()
422
+ click.echo(click.style(
423
+ f" Available tools: {[t.name for t in tools.tools]}",
424
+ fg="bright_black"
425
+ ))
426
+
427
+ # ── Step 2: Get git diff ────────────────────────
428
+ click.echo(click.style("\nπŸ“‚ Fetching git diff (changes against HEAD)...", fg="cyan"))
429
+
430
+ diff_result = await session.call_tool(
431
+ "git_diff",
432
+ arguments={
433
+ "repo_path": repo_path,
434
+ "target": DEFAULT_DIFF_TARGET,
435
+ },
436
+ )
437
+
438
+ diff_text = extract_tool_text(diff_result)
439
+
440
+ if getattr(diff_result, "isError", False):
441
+ raise click.ClickException(
442
+ f"git_diff failed: {diff_text or 'Unknown MCP tool error.'}"
443
+ )
444
+
445
+ if not diff_text.strip():
446
+ click.echo(click.style(
447
+ "⚠️ No changes detected. Nothing to commit.",
448
+ fg="yellow"
449
+ ))
450
+ return
451
+
452
+ if verbose:
453
+ click.echo(click.style("\n── Diff Preview ──", fg="bright_black"))
454
+ preview = diff_text[:1500] + ("..." if len(diff_text) > 1500 else "")
455
+ click.echo(click.style(preview, fg="bright_black"))
456
+
457
+ click.echo(click.style(
458
+ f" Diff captured ({len(diff_text)} chars).", fg="green"
459
+ ))
460
+
461
+ # ── Step 3: Generate commit message via LLM ─────
462
+ config = load_config()
463
+ provider = config.get("provider", "gemini")
464
+ click.echo(click.style(f"\nπŸ€– Generating commit message suggestions with {provider.upper()}...", fg="cyan"))
465
+
466
+ llm_diff_payload, was_truncated = build_llm_diff_payload(diff_text)
467
+ if was_truncated:
468
+ click.echo(click.style(
469
+ f" Large diff detected. Sending compacted payload ({len(llm_diff_payload)} chars) to AI.",
470
+ fg="yellow"
471
+ ))
472
+
473
+ suggestions = generate_commit_suggestions(
474
+ llm_diff_payload,
475
+ repo_path,
476
+ suggestion_count,
477
+ )
478
+ if len(suggestions) < suggestion_count and verbose:
479
+ click.echo(click.style(
480
+ f" Provider returned {len(suggestions)} valid suggestion(s).",
481
+ fg="yellow",
482
+ ))
483
+
484
+ click.echo(click.style("\nπŸ’¬ Suggested Commit Messages:", fg="green"))
485
+ for idx, suggestion in enumerate(suggestions, start=1):
486
+ click.echo(click.style(f" {idx}. {suggestion}", fg="bright_white"))
487
+
488
+ selection = click.prompt(
489
+ click.style("\nSelect message number or type a custom message", fg="yellow"),
490
+ default="1",
491
+ type=str,
492
+ ).strip()
493
+ if selection.isdigit() and 1 <= int(selection) <= len(suggestions):
494
+ commit_message = suggestions[int(selection) - 1]
495
+ elif selection:
496
+ commit_message = selection
497
+ else:
498
+ commit_message = suggestions[0]
499
+
500
+ click.echo(click.style(
501
+ f"\nβœ… Selected Commit Message: {click.style(commit_message, bold=True, fg='bright_white')}",
502
+ fg="green"
503
+ ))
504
+
505
+ # ── Confirmation Prompt ─────────────────────────
506
+ if not click.confirm(
507
+ click.style("\nProceed with git add + commit?", fg="yellow"),
508
+ default=True,
509
+ ):
510
+ click.echo(click.style("❌ Aborted by user.", fg="red"))
511
+ return
512
+
513
+ # ── Step 4: git add . ───────────────────────────
514
+ click.echo(click.style("\nπŸ“¦ Staging all changes (git add .)...", fg="cyan"))
515
+
516
+ add_result = await session.call_tool(
517
+ "git_add",
518
+ arguments={
519
+ "repo_path": repo_path,
520
+ "files": ["."],
521
+ },
522
+ )
523
+
524
+ add_output = ""
525
+ for block in add_result.content:
526
+ if hasattr(block, "text"):
527
+ add_output += block.text
528
+ click.echo(click.style(f" {add_output.strip() or 'Files staged.'}", fg="green"))
529
+
530
+ # ── Step 5: git commit ──────────────────────────
531
+ click.echo(click.style("\n✍️ Committing...", fg="cyan"))
532
+
533
+ commit_result = await session.call_tool(
534
+ "git_commit",
535
+ arguments={
536
+ "repo_path": repo_path,
537
+ "message": commit_message,
538
+ },
539
+ )
540
+
541
+ commit_output = ""
542
+ for block in commit_result.content:
543
+ if hasattr(block, "text"):
544
+ commit_output += block.text
545
+ click.echo(click.style(f" {commit_output.strip()}", fg="green"))
546
+
547
+ click.echo(click.style(
548
+ "\nπŸš€ Done! Changes committed successfully.",
549
+ fg="bright_green", bold=True
550
+ ))
551
+
552
+
553
+ # ─────────────────────────────────────────────
554
+ # CLI Entry Points
555
+ # ─────────────────────────────────────────────
556
+
557
+ @click.group()
558
+ @click.version_option(version="0.1.0", prog_name="agent-gitv1")
559
+ def cli():
560
+ """
561
+ \b
562
+ ╔════════════════════════════════════════════╗
563
+ β•‘ agent-gitv1 πŸ€– MCP + Multi-LLM Commits β•‘
564
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
565
+
566
+ Automate your Git workflow with AI-generated commit messages.
567
+ Supports OpenAI, Gemini, and Local Ollama models!
568
+ """
569
+ pass
570
+
571
+ @cli.command("config")
572
+ def config_command():
573
+ """Configure your preferred AI Provider (OpenAI, Gemini, or Ollama)."""
574
+ click.echo(click.style("πŸ›  Agent Git Setup\n", bold=True, fg="cyan"))
575
+
576
+ provider = click.prompt(
577
+ "Select AI Provider",
578
+ type=click.Choice(["gemini", "openai", "ollama"]),
579
+ default="ollama"
580
+ )
581
+
582
+ config = {"provider": provider}
583
+
584
+ if provider == "gemini":
585
+ config["api_key"] = click.prompt("Gemini API Key (Leave empty to use GEMINI_API_KEY env var)", default="", hide_input=True)
586
+ config["model"] = click.prompt("Model", default="gemini-2.0-flash")
587
+
588
+ elif provider == "openai":
589
+ config["api_key"] = click.prompt("OpenAI API Key (Leave empty to use OPENAI_API_KEY env var)", default="", hide_input=True)
590
+ config["model"] = click.prompt("Model", default="gpt-4o-mini")
591
+
592
+ elif provider == "ollama":
593
+ base_url = click.prompt("Ollama Base URL", default="http://localhost:11434")
594
+ config["base_url"] = base_url
595
+
596
+ click.echo("Fetching available models from Ollama...")
597
+ models = get_ollama_models(base_url)
598
+
599
+ if models:
600
+ click.echo(click.style("\nAvailable Ollama Models:", fg="green"))
601
+ for i, m in enumerate(models):
602
+ click.echo(f" {i+1}. {m}")
603
+
604
+ choice = click.prompt("\nSelect a model by number or type model name manually", type=str)
605
+ if choice.isdigit() and 1 <= int(choice) <= len(models):
606
+ config["model"] = models[int(choice)-1]
607
+ else:
608
+ config["model"] = choice
609
+ else:
610
+ click.echo(click.style("Could not auto-fetch models.", fg="yellow"))
611
+ config["model"] = click.prompt("Model Name (e.g. llama3:8b, mistral, deepseek-r1)", default="llama3:latest")
612
+
613
+ save_config(config)
614
+ click.echo(click.style(f"\nβœ… Configuration saved successfully to {CONFIG_FILE}!", fg="bright_green", bold=True))
615
+ click.echo(click.style(f"Current setup -> Provider: {provider}, Model: {config['model']}", fg="green"))
616
+
617
+
618
+ @cli.command("commit")
619
+ @click.option(
620
+ "--repo",
621
+ "-r",
622
+ default=".",
623
+ show_default=True,
624
+ help="Path to the git repository.",
625
+ )
626
+ @click.option(
627
+ "--verbose",
628
+ "-v",
629
+ is_flag=True,
630
+ default=False,
631
+ help="Show extra debug information.",
632
+ )
633
+ @click.option(
634
+ "--suggestions",
635
+ "-s",
636
+ type=click.IntRange(1, 5),
637
+ default=3,
638
+ show_default=True,
639
+ help="How many AI commit message suggestions to generate.",
640
+ )
641
+ def commit_command(repo: str, verbose: bool, suggestions: int):
642
+ """Stage, generate a commit message, and commit all changes."""
643
+ repo_path = os.path.abspath(repo)
644
+
645
+ if not os.path.isdir(os.path.join(repo_path, ".git")):
646
+ raise click.ClickException(
647
+ f"'{repo_path}' is not a valid git repository (no .git folder found)."
648
+ )
649
+
650
+ click.echo(click.style(
651
+ f"\nπŸ—‚ Repository: {repo_path}", fg="bright_cyan", bold=True
652
+ ))
653
+
654
+ try:
655
+ asyncio.run(run_agent_commit(repo_path, verbose, suggestions))
656
+ except KeyboardInterrupt:
657
+ click.echo(click.style("\n\n⚑ Interrupted by user.", fg="yellow"))
658
+ sys.exit(0)
659
+ except Exception as e:
660
+ raise click.ClickException(str(e))
661
+
662
+
663
+ @cli.command("push")
664
+ @click.option("--remote", default="origin", show_default=True, help="Remote name.")
665
+ @click.option("--branch", default=None, help="Branch to push (defaults to current).")
666
+ @click.option("--repo", "-r", default=".", show_default=True, help="Repo path.")
667
+ def push_command(remote: str, branch: str, repo: str):
668
+ """Push committed changes to the remote repository."""
669
+ import subprocess
670
+ repo_path = os.path.abspath(repo)
671
+
672
+ if not os.path.isdir(os.path.join(repo_path, ".git")):
673
+ raise click.ClickException(f"'{repo_path}' is not a valid git repository.")
674
+
675
+ # Check if a remote exists
676
+ try:
677
+ remotes = subprocess.check_output(["git", "remote", "-v"], cwd=repo_path, text=True)
678
+ if not remotes.strip():
679
+ raise click.ClickException(
680
+ "No git remotes configured! You need to add a remote first.\n"
681
+ "Example: git remote add origin https://github.com/user/repo.git"
682
+ )
683
+ except subprocess.CalledProcessError:
684
+ raise click.ClickException("Failed to check git remotes.")
685
+
686
+ click.echo(click.style(
687
+ f"πŸš€ Pushing to {remote} {branch or '(current branch)'}...", fg="cyan"
688
+ ))
689
+
690
+ cmd = ["git", "push", remote]
691
+ if branch:
692
+ cmd.append(branch)
693
+
694
+ try:
695
+ # We run it directly so SSH / password prompts work nicely in the terminal
696
+ result = subprocess.run(cmd, cwd=repo_path)
697
+ if result.returncode == 0:
698
+ click.echo(click.style("\nβœ… Push complete!", fg="bright_green", bold=True))
699
+ else:
700
+ sys.exit(result.returncode)
701
+ except KeyboardInterrupt:
702
+ click.echo(click.style("\n⚑ Interrupted.", fg="yellow"))
703
+ except Exception as e:
704
+ raise click.ClickException(f"Failed to execute git push: {e}")
705
+
706
+
707
+ # ─────────────────────────────────────────────
708
+ # Entrypoint
709
+ # ─────────────────────────────────────────────
710
+
711
+ def main():
712
+ cli()
713
+
714
+
715
+ if __name__ == "__main__":
716
+ main()
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-gitv1
3
+ Version: 0.1.0
4
+ Summary: AI-powered Git CLI using MCP + Gemini to auto-generate commit messages
5
+ Author-email: Vijay <you@example.com>
6
+ License: MIT
7
+ Keywords: ai,automation,cli,gemini,git,mcp
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: google-genai>=0.8.0
11
+ Requires-Dist: mcp[cli]>=1.0.0
12
+ Requires-Dist: openai>=1.14.0
13
+ Requires-Dist: requests>=2.31.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # agent-gitv1 πŸ€–
17
+
18
+ > **AI-powered Git CLI** β€” Uses [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) + Google Gemini to automatically generate professional commit messages from your diff.
19
+
20
+ ---
21
+
22
+ ## How It Works
23
+
24
+ ```
25
+ Your Code Changes
26
+ β”‚
27
+ β–Ό
28
+ [MCP Client] ──stdio──► [mcp-server-git]
29
+ β”‚ β”‚
30
+ │◄── git diff β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
31
+ β”‚
32
+ β–Ό
33
+ [Gemini AI] ──► Generate commit message
34
+ β”‚
35
+ β–Ό
36
+ [MCP Client] ──► git add . ──► git commit
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Prerequisites
42
+
43
+ | Tool | Install |
44
+ |------|---------|
45
+ | Python β‰₯ 3.10 | [python.org](https://python.org) |
46
+ | `uvx` (uv tool runner) | `pip install uv` |
47
+ | `mcp-server-git` | Auto-fetched by `uvx` |
48
+ | Gemini API Key | [aistudio.google.com](https://aistudio.google.com) |
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ # 1. Clone / navigate to the project
56
+ cd "d:\Vijay Projects\Agent_bhai"
57
+
58
+ # 2. Install in editable mode (creates the `agent` command globally)
59
+ pip install -e .
60
+
61
+ # 3. Set your Gemini API key
62
+ set GEMINI_API_KEY=your_gemini_api_key_here # Windows CMD
63
+ $env:GEMINI_API_KEY="your_key" # Windows PowerShell
64
+ export GEMINI_API_KEY=your_gemini_api_key_here # Linux / Mac
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Usage
70
+
71
+ ### `agent config` β€” Configure LLM Provider (Start Here!)
72
+
73
+ Run this to configure OpenAI, Google Gemini, or Ollama.
74
+
75
+ ```bash
76
+ agent config
77
+ ```
78
+
79
+ What you can configure:
80
+ - **Google Gemini**: Uses your API key and a model like `gemini-2.0-flash`.
81
+ - **OpenAI (ChatGPT)**: Uses your API key and a model like `gpt-4o-mini`.
82
+ - **Ollama (Local/Network)**: Provide the base URL (e.g. `http://localhost:11434` or `http://192.168.1.50:11434`). The CLI will automatically fetch your downloaded models and let you choose one!
83
+
84
+ ---
85
+
86
+ ### `agent commit` β€” Stage + AI commit message + commit
87
+
88
+ ```bash
89
+ # In any git repo:
90
+ agent commit
91
+
92
+ # Specify a repo path:
93
+ agent commit --repo /path/to/repo
94
+
95
+ # Generate multiple suggestions (pick one or type your own):
96
+ agent commit --suggestions 3
97
+
98
+ # Verbose mode (shows diff preview + available MCP tools):
99
+ agent commit --verbose
100
+ agent commit -v
101
+ ```
102
+
103
+ `agent commit` is now history-aware:
104
+ - It uses recent commit messages from your repo to align tone/style.
105
+ - It includes changed file names in the LLM prompt for better scoped messages.
106
+
107
+ ### `agent push` β€” Push to remote
108
+
109
+ ```bash
110
+ agent push
111
+ agent push --remote origin --branch main
112
+ ```
113
+
114
+ ### Help
115
+
116
+ ```bash
117
+ agent --help
118
+ agent config --help
119
+ agent commit --help
120
+ agent push --help
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Example Session
126
+
127
+ ```
128
+ πŸ—‚ Repository: D:\my-project
129
+
130
+ πŸ”Œ Connecting to mcp-server-git...
131
+ βœ… MCP session initialized.
132
+
133
+ πŸ“‚ Fetching git diff (unstaged changes)...
134
+ Diff captured (1240 chars).
135
+
136
+ πŸ€– Generating commit message with Gemini...
137
+
138
+ πŸ’¬ Commit Message: feat(auth): add JWT token refresh endpoint
139
+
140
+ Proceed with git add + commit? [Y/n]: y
141
+
142
+ πŸ“¦ Staging all changes (git add .)...
143
+ Files staged.
144
+
145
+ ✍️ Committing...
146
+ [main a3f12bc] feat(auth): add JWT token refresh endpoint
147
+
148
+ πŸš€ Done! Changes committed successfully.
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Environment Variables
154
+
155
+ | Variable | Required | Description |
156
+ |----------|----------|-------------|
157
+ | `GEMINI_API_KEY` | βœ… Yes | Your Google Gemini API key |
158
+
159
+ ---
160
+
161
+ ## Project Structure
162
+
163
+ ```
164
+ Agent_bhai/
165
+ β”œβ”€β”€ agent.py # Main CLI + MCP client logic
166
+ β”œβ”€β”€ pyproject.toml # Packaging + entry point config
167
+ └── README.md # This file
168
+ ```
169
+
170
+ ---
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,8 @@
1
+ ./PKG-INFO,sha256=3ALyKD-dRyRpnDjxZfSJMMQPOqDEhEkQzjoc43-efL0,3927
2
+ ./README.md,sha256=-CrDrXOXqj7_2dkk_VXRZTUx9E4ZEXu3G5Ti3naLY7w,3479
3
+ ./agent.py,sha256=xr89SLvDhSQcGbGvz624kcCPXGzKJbzuTy_mQ6xbPlE,27165
4
+ ./pyproject.toml,sha256=4hDiJymLhMU0SL6ETqSOcbusbwQNGU6zSQhe9A9crJc,769
5
+ agent_gitv1-0.1.0.dist-info/METADATA,sha256=3ALyKD-dRyRpnDjxZfSJMMQPOqDEhEkQzjoc43-efL0,3927
6
+ agent_gitv1-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ agent_gitv1-0.1.0.dist-info/entry_points.txt,sha256=cbPAl9C0KyMpkeNPgYr9JclssqVmdDzFPmq0DQ4PmAs,37
8
+ agent_gitv1-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent = agent:main
pyproject.toml ADDED
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agent-gitv1"
7
+ version = "0.1.0"
8
+ description = "AI-powered Git CLI using MCP + Gemini to auto-generate commit messages"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Vijay", email = "you@example.com" }
14
+ ]
15
+ keywords = ["git", "mcp", "gemini", "ai", "cli", "automation"]
16
+
17
+ dependencies = [
18
+ "click>=8.1",
19
+ "mcp[cli]>=1.0.0",
20
+ "google-genai>=0.8.0",
21
+ "openai>=1.14.0",
22
+ "requests>=2.31.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ # This is the magic line:
27
+ # Typing `agent` in terminal calls the `main()` function in agent.py
28
+ agent = "agent:main"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["."]
32
+
33
+ [tool.hatch.build]
34
+ include = [
35
+ "agent.py",
36
+ ]