skydeckai-code 0.1.23__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.
- aidd/__init__.py +11 -0
- aidd/cli.py +141 -0
- aidd/server.py +54 -0
- aidd/tools/__init__.py +155 -0
- aidd/tools/base.py +18 -0
- aidd/tools/code_analysis.py +703 -0
- aidd/tools/code_execution.py +321 -0
- aidd/tools/directory_tools.py +289 -0
- aidd/tools/file_tools.py +784 -0
- aidd/tools/get_active_apps_tool.py +455 -0
- aidd/tools/get_available_windows_tool.py +395 -0
- aidd/tools/git_tools.py +687 -0
- aidd/tools/image_tools.py +127 -0
- aidd/tools/path_tools.py +86 -0
- aidd/tools/screenshot_tool.py +1029 -0
- aidd/tools/state.py +47 -0
- aidd/tools/system_tools.py +190 -0
- skydeckai_code-0.1.23.dist-info/METADATA +628 -0
- skydeckai_code-0.1.23.dist-info/RECORD +22 -0
- skydeckai_code-0.1.23.dist-info/WHEEL +4 -0
- skydeckai_code-0.1.23.dist-info/entry_points.txt +3 -0
- skydeckai_code-0.1.23.dist-info/licenses/LICENSE +201 -0
aidd/tools/git_tools.py
ADDED
@@ -0,0 +1,687 @@
|
|
1
|
+
import os
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
import git
|
5
|
+
from mcp.types import TextContent
|
6
|
+
|
7
|
+
from .state import state
|
8
|
+
|
9
|
+
|
10
|
+
def _get_repo(repo_path: str) -> git.Repo:
|
11
|
+
"""Helper function to get git repo with validation."""
|
12
|
+
# Determine full path based on whether input is absolute or relative
|
13
|
+
if os.path.isabs(repo_path):
|
14
|
+
full_path = os.path.abspath(repo_path)
|
15
|
+
else:
|
16
|
+
full_path = os.path.abspath(os.path.join(state.allowed_directory, repo_path))
|
17
|
+
|
18
|
+
# Security check
|
19
|
+
if not full_path.startswith(state.allowed_directory):
|
20
|
+
raise ValueError(f"Access denied: Path ({full_path}) must be within allowed directory")
|
21
|
+
|
22
|
+
try:
|
23
|
+
return git.Repo(full_path)
|
24
|
+
except git.InvalidGitRepositoryError:
|
25
|
+
raise ValueError(f"Not a valid git repository: {full_path}")
|
26
|
+
except Exception as e:
|
27
|
+
raise ValueError(f"Error accessing git repository at '{full_path}': {str(e)}")
|
28
|
+
|
29
|
+
def git_init_tool():
|
30
|
+
return {
|
31
|
+
"name": "git_init",
|
32
|
+
"description": "Initialize a new Git repository. "
|
33
|
+
"WHEN TO USE: When you need to set up version control for a project, "
|
34
|
+
"start tracking files with Git, or begin a new Git-based workflow. "
|
35
|
+
"Useful for new projects or for adding version control to existing code. "
|
36
|
+
"WHEN NOT TO USE: When a Git repository already exists in the target location. "
|
37
|
+
"RETURNS: A confirmation message indicating that the Git repository was initialized "
|
38
|
+
"successfully, including the path and initial branch name. If the specified directory "
|
39
|
+
"doesn't exist, it will be created automatically. Directory must be within the allowed workspace.",
|
40
|
+
"inputSchema": {
|
41
|
+
"type": "object",
|
42
|
+
"properties": {
|
43
|
+
"path": {
|
44
|
+
"type": "string",
|
45
|
+
"description": "Path where to initialize the repository. This will be the root directory of the Git "
|
46
|
+
"repository. Examples: '.', 'my-project', 'src/module'. Both absolute and relative "
|
47
|
+
"paths are supported, but must be within the allowed workspace."
|
48
|
+
},
|
49
|
+
"initial_branch": {
|
50
|
+
"type": "string",
|
51
|
+
"description": "Name of the initial branch to create. Common values are 'main' (modern default) or "
|
52
|
+
"'master' (legacy default). Examples: 'main', 'develop', 'trunk'. If not specified, "
|
53
|
+
"'main' will be used.",
|
54
|
+
"default": "main"
|
55
|
+
}
|
56
|
+
},
|
57
|
+
"required": ["path"]
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
async def handle_git_init(arguments: dict) -> List[TextContent]:
|
62
|
+
"""Handle initializing a new git repository."""
|
63
|
+
path = arguments["path"]
|
64
|
+
initial_branch = arguments.get("initial_branch", "main")
|
65
|
+
|
66
|
+
# Validate and create directory if needed
|
67
|
+
full_path = os.path.abspath(os.path.join(state.allowed_directory, path))
|
68
|
+
if not full_path.startswith(state.allowed_directory):
|
69
|
+
raise ValueError(f"Access denied: Path ({full_path}) must be within allowed directory")
|
70
|
+
|
71
|
+
try:
|
72
|
+
os.makedirs(full_path, exist_ok=True)
|
73
|
+
git.Repo.init(full_path, initial_branch=initial_branch)
|
74
|
+
return [TextContent(
|
75
|
+
type="text",
|
76
|
+
text=f"Initialized empty Git repository in {path} with initial branch '{initial_branch}'"
|
77
|
+
)]
|
78
|
+
except Exception as e:
|
79
|
+
raise ValueError(f"Error initializing repository at '{full_path}': {str(e)}")
|
80
|
+
|
81
|
+
def git_status_tool():
|
82
|
+
return {
|
83
|
+
"name": "git_status",
|
84
|
+
"description": "Shows the working tree status of a git repository. "
|
85
|
+
"WHEN TO USE: When you need to check what files have been modified, added, or deleted in a repository, "
|
86
|
+
"understand the current state of the working directory, or determine which files are staged for commit. "
|
87
|
+
"Useful before committing changes, when troubleshooting repository state, or for confirming which "
|
88
|
+
"files have been modified. "
|
89
|
+
"WHEN NOT TO USE: When you need to see the actual content changes (use git_diff_unstaged or git_diff_staged instead), "
|
90
|
+
"when you need to view commit history (use git_log instead), or when you need information about a specific "
|
91
|
+
"commit (use git_show instead). "
|
92
|
+
"RETURNS: Text output showing the current branch, tracking information, and status of all files in the "
|
93
|
+
"repository, including staged, unstaged, and untracked files. Repository must be within the allowed directory.",
|
94
|
+
"inputSchema": {
|
95
|
+
"type": "object",
|
96
|
+
"properties": {
|
97
|
+
"repo_path": {
|
98
|
+
"type": "string",
|
99
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
100
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
101
|
+
"are supported, but must be within the allowed workspace."
|
102
|
+
}
|
103
|
+
},
|
104
|
+
"required": ["repo_path"]
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
def git_diff_unstaged_tool():
|
109
|
+
return {
|
110
|
+
"name": "git_diff_unstaged",
|
111
|
+
"description": "Shows changes in working directory not yet staged for commit. "
|
112
|
+
"WHEN TO USE: When you need to see the exact content changes that have been made to files but not yet "
|
113
|
+
"added to the staging area. Useful for reviewing modifications before staging them, understanding what "
|
114
|
+
"changes have been made since the last commit, or inspecting file differences in detail. "
|
115
|
+
"WHEN NOT TO USE: When you only need to know which files have been modified without seeing the changes "
|
116
|
+
"(use git_status instead), when you want to see changes that are already staged (use git_diff_staged instead), "
|
117
|
+
"or when you want to compare with a specific branch (use git_diff instead). "
|
118
|
+
"RETURNS: A unified diff output showing the exact changes made to files that have not yet been staged, "
|
119
|
+
"including added, deleted, and modified lines with proper context. If no unstaged changes exist, it will "
|
120
|
+
"indicate that. Repository must be within the allowed directory.",
|
121
|
+
"inputSchema": {
|
122
|
+
"type": "object",
|
123
|
+
"properties": {
|
124
|
+
"repo_path": {
|
125
|
+
"type": "string",
|
126
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
127
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
128
|
+
"are supported, but must be within the allowed workspace."
|
129
|
+
}
|
130
|
+
},
|
131
|
+
"required": ["repo_path"]
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
def git_diff_staged_tool():
|
136
|
+
return {
|
137
|
+
"name": "git_diff_staged",
|
138
|
+
"description": "Shows changes staged for commit. "
|
139
|
+
"WHEN TO USE: When you need to see the exact content changes that have been added to the staging area "
|
140
|
+
"and are ready to be committed. Useful for reviewing modifications before committing them, verifying "
|
141
|
+
"that the right changes are staged, or inspecting file differences in detail. "
|
142
|
+
"WHEN NOT TO USE: When you only need to know which files are staged without seeing the changes "
|
143
|
+
"(use git_status instead), when you want to see changes that are not yet staged (use git_diff_unstaged instead), "
|
144
|
+
"or when you want to compare with a specific branch (use git_diff instead). "
|
145
|
+
"RETURNS: A unified diff output showing the exact changes that have been staged, including added, "
|
146
|
+
"deleted, and modified lines with proper context. If no staged changes exist, it will indicate that. "
|
147
|
+
"Repository must be within the allowed directory.",
|
148
|
+
"inputSchema": {
|
149
|
+
"type": "object",
|
150
|
+
"properties": {
|
151
|
+
"repo_path": {
|
152
|
+
"type": "string",
|
153
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
154
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
155
|
+
"are supported, but must be within the allowed workspace."
|
156
|
+
}
|
157
|
+
},
|
158
|
+
"required": ["repo_path"]
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
def git_diff_tool():
|
163
|
+
return {
|
164
|
+
"name": "git_diff",
|
165
|
+
"description": "Shows differences between branches or commits. "
|
166
|
+
"WHEN TO USE: When you need to compare the current branch with another branch or commit, "
|
167
|
+
"see what changes were made in a specific branch, or analyze differences between different "
|
168
|
+
"versions of the code. Useful for code reviews, understanding changes between versions, or "
|
169
|
+
"preparing for merges. "
|
170
|
+
"WHEN NOT TO USE: When you want to see only unstaged changes (use git_diff_unstaged instead), "
|
171
|
+
"when you want to see only staged changes (use git_diff_staged instead), or when you just need "
|
172
|
+
"a list of changed files without content details (use git_status instead). "
|
173
|
+
"RETURNS: A unified diff output showing the exact differences between the current branch and "
|
174
|
+
"the specified target branch or commit, including added, deleted, and modified lines with proper "
|
175
|
+
"context. If no differences exist, it will indicate that. Repository must be within the allowed directory.",
|
176
|
+
"inputSchema": {
|
177
|
+
"type": "object",
|
178
|
+
"properties": {
|
179
|
+
"repo_path": {
|
180
|
+
"type": "string",
|
181
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
182
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
183
|
+
"are supported, but must be within the allowed workspace."
|
184
|
+
},
|
185
|
+
"target": {
|
186
|
+
"type": "string",
|
187
|
+
"description": "Target branch or commit to compare with the current branch. This can be a branch name, "
|
188
|
+
"commit hash, or reference like HEAD~1. Examples: 'main', 'develop', 'feature/new-feature', "
|
189
|
+
"'a1b2c3d' (commit hash)."
|
190
|
+
}
|
191
|
+
},
|
192
|
+
"required": ["repo_path", "target"]
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
def git_commit_tool():
|
197
|
+
return {
|
198
|
+
"name": "git_commit",
|
199
|
+
"description": "Records changes to the repository by creating a new commit. "
|
200
|
+
"WHEN TO USE: When you have staged changes that you want to permanently record in the repository history, "
|
201
|
+
"after using git_add to stage your changes, or when you've completed a logical unit of work that should "
|
202
|
+
"be captured. Useful after reviewing staged changes with git_diff_staged and confirming they're ready to commit. "
|
203
|
+
"WHEN NOT TO USE: When you haven't staged any changes yet (use git_add first), when you want to see what "
|
204
|
+
"changes would be committed (use git_diff_staged instead), or when you want to undo staged changes "
|
205
|
+
"(use git_reset instead). "
|
206
|
+
"RETURNS: A confirmation message with the commit hash if successful, or a message indicating that there "
|
207
|
+
"are no changes staged for commit. For new repositories without commits, checks if there are staged files. "
|
208
|
+
"Repository must be within the allowed directory.",
|
209
|
+
"inputSchema": {
|
210
|
+
"type": "object",
|
211
|
+
"properties": {
|
212
|
+
"repo_path": {
|
213
|
+
"type": "string",
|
214
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
215
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
216
|
+
"are supported, but must be within the allowed workspace."
|
217
|
+
},
|
218
|
+
"message": {
|
219
|
+
"type": "string",
|
220
|
+
"description": "Commit message that describes the changes being committed. This message will be permanently "
|
221
|
+
"recorded in the repository history and should clearly describe what changes were made and why. "
|
222
|
+
"Examples: 'Fix bug in login function', 'Add pagination to user list', 'Update documentation for API endpoints'."
|
223
|
+
}
|
224
|
+
},
|
225
|
+
"required": ["repo_path", "message"]
|
226
|
+
}
|
227
|
+
}
|
228
|
+
|
229
|
+
def git_add_tool():
|
230
|
+
return {
|
231
|
+
"name": "git_add",
|
232
|
+
"description": "Adds file contents to the staging area. "
|
233
|
+
"WHEN TO USE: When you want to prepare modified files for commit, select specific files to include "
|
234
|
+
"in the next commit, or track new files in the repository. This is typically the first step in the "
|
235
|
+
"commit workflow after making changes. "
|
236
|
+
"WHEN NOT TO USE: When you want to undo staging (use git_reset instead), or when there are no changes to stage. "
|
237
|
+
"RETURNS: A confirmation message listing the files that were successfully staged for commit. "
|
238
|
+
"Repository must be within the allowed directory.",
|
239
|
+
"inputSchema": {
|
240
|
+
"type": "object",
|
241
|
+
"properties": {
|
242
|
+
"repo_path": {
|
243
|
+
"type": "string",
|
244
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
245
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
246
|
+
"are supported, but must be within the allowed workspace."
|
247
|
+
},
|
248
|
+
"files": {
|
249
|
+
"type": "array",
|
250
|
+
"items": {"type": "string"},
|
251
|
+
"description": "List of file paths to stage for the next commit. These paths should be relative to the "
|
252
|
+
"repository root. Can include specific files, directories, or patterns. "
|
253
|
+
"Examples: ['README.md'], ['src/main.py', 'docs/index.html'], ['*.js'] to stage all "
|
254
|
+
"JavaScript files in the current directory."
|
255
|
+
}
|
256
|
+
},
|
257
|
+
"required": ["repo_path", "files"]
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
def git_reset_tool():
|
262
|
+
return {
|
263
|
+
"name": "git_reset",
|
264
|
+
"description": "Unstages all staged changes. "
|
265
|
+
"WHEN TO USE: When you want to undo staging operations, remove files from the staging area without "
|
266
|
+
"losing changes, or start over with your staging selections. Useful when you accidentally staged files "
|
267
|
+
"that shouldn't be committed or need to reorganize what will be included in the next commit. "
|
268
|
+
"WHEN NOT TO USE: When you want to keep some staged changes (this tool unstages everything), when you "
|
269
|
+
"want to discard changes completely (not just unstage them), or when you need to modify commit history. "
|
270
|
+
"RETURNS: A confirmation message indicating that all changes have been successfully unstaged. For new "
|
271
|
+
"repositories without commits, it removes all files from the index. "
|
272
|
+
"Repository must be within the allowed directory.",
|
273
|
+
"inputSchema": {
|
274
|
+
"type": "object",
|
275
|
+
"properties": {
|
276
|
+
"repo_path": {
|
277
|
+
"type": "string",
|
278
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
279
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
280
|
+
"are supported, but must be within the allowed workspace."
|
281
|
+
}
|
282
|
+
},
|
283
|
+
"required": ["repo_path"]
|
284
|
+
}
|
285
|
+
}
|
286
|
+
|
287
|
+
def git_log_tool():
|
288
|
+
return {
|
289
|
+
"name": "git_log",
|
290
|
+
"description": "Shows the commit logs. "
|
291
|
+
"WHEN TO USE: When you need to view the commit history of a repository, check who made specific changes, "
|
292
|
+
"understand the evolution of a project over time, or find a specific commit by its description. Useful "
|
293
|
+
"for investigating the project history, finding when features were added, or understanding code changes. "
|
294
|
+
"WHEN NOT TO USE: When you need to see the actual changes in a commit (use git_show instead), when you "
|
295
|
+
"need to compare branches (use git_diff instead), or when you just want to know the current repository "
|
296
|
+
"status (use git_status instead). "
|
297
|
+
"RETURNS: Text output listing commit history with details for each commit including hash, author, date, "
|
298
|
+
"and commit message. For new repositories, indicates that no commits exist yet. "
|
299
|
+
"Repository must be within the allowed directory.",
|
300
|
+
"inputSchema": {
|
301
|
+
"type": "object",
|
302
|
+
"properties": {
|
303
|
+
"repo_path": {
|
304
|
+
"type": "string",
|
305
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
306
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
307
|
+
"are supported, but must be within the allowed workspace."
|
308
|
+
},
|
309
|
+
"max_count": {
|
310
|
+
"type": "integer",
|
311
|
+
"description": "Maximum number of commits to show in the history. This limits the output to the most "
|
312
|
+
"recent N commits. Examples: 5 to show the last five commits, 20 to show more history. "
|
313
|
+
"Higher values may result in longer output. If not specified, defaults to 10.",
|
314
|
+
"default": 10
|
315
|
+
}
|
316
|
+
},
|
317
|
+
"required": ["repo_path"]
|
318
|
+
}
|
319
|
+
}
|
320
|
+
|
321
|
+
def git_create_branch_tool():
|
322
|
+
return {
|
323
|
+
"name": "git_create_branch",
|
324
|
+
"description": "Creates a new branch in a git repository. "
|
325
|
+
"WHEN TO USE: When you need to start working on a new feature, create an isolated environment for "
|
326
|
+
"development, branch off from the main codebase, or prepare for a pull request. Useful for implementing "
|
327
|
+
"features without affecting the main codebase, fixing bugs in isolation, or managing parallel development "
|
328
|
+
"workflows. "
|
329
|
+
"WHEN NOT TO USE: When the repository has no commits yet (make an initial commit first), when you just "
|
330
|
+
"want to switch to an existing branch (use git_checkout instead), or when you want to see differences "
|
331
|
+
"between branches (use git_diff instead). "
|
332
|
+
"RETURNS: A confirmation message indicating that the branch was successfully created and which branch "
|
333
|
+
"it was created from. Requires that the repository has at least one commit. "
|
334
|
+
"Repository must be within the allowed directory.",
|
335
|
+
"inputSchema": {
|
336
|
+
"type": "object",
|
337
|
+
"properties": {
|
338
|
+
"repo_path": {
|
339
|
+
"type": "string",
|
340
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
341
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
342
|
+
"are supported, but must be within the allowed workspace."
|
343
|
+
},
|
344
|
+
"branch_name": {
|
345
|
+
"type": "string",
|
346
|
+
"description": "Name of the new branch to create. Should follow git branch naming conventions. "
|
347
|
+
"Examples: 'feature/user-auth', 'bugfix/login-issue', 'release/1.0.0'."
|
348
|
+
},
|
349
|
+
"base_branch": {
|
350
|
+
"type": "string",
|
351
|
+
"description": "Starting point for the new branch. This can be a branch name, tag, or commit hash from "
|
352
|
+
"which the new branch will be created. If not specified, the current active branch is "
|
353
|
+
"used as the base. Examples: 'main', 'develop', 'v1.0' (tag), 'a1b2c3d' (commit hash).",
|
354
|
+
"default": None
|
355
|
+
}
|
356
|
+
},
|
357
|
+
"required": ["repo_path", "branch_name"]
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
def git_checkout_tool():
|
362
|
+
return {
|
363
|
+
"name": "git_checkout",
|
364
|
+
"description": "Switches branches in a git repository. "
|
365
|
+
"WHEN TO USE: When you need to switch your working directory to a different branch, view or work on "
|
366
|
+
"code from another branch, or prepare for merging branches. Useful for moving between feature branches, "
|
367
|
+
"switching to the main branch to pull updates, or starting work on a different task. "
|
368
|
+
"WHEN NOT TO USE: When the repository has no commits yet (make an initial commit first), when you want "
|
369
|
+
"to create a new branch (use git_create_branch instead), or when you have uncommitted changes that would "
|
370
|
+
"be overwritten. "
|
371
|
+
"RETURNS: A confirmation message indicating that the branch was successfully checked out. Requires that "
|
372
|
+
"the repository has at least one commit and that the specified branch exists. "
|
373
|
+
"Repository must be within the allowed directory.",
|
374
|
+
"inputSchema": {
|
375
|
+
"type": "object",
|
376
|
+
"properties": {
|
377
|
+
"repo_path": {
|
378
|
+
"type": "string",
|
379
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
380
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
381
|
+
"are supported, but must be within the allowed workspace."
|
382
|
+
},
|
383
|
+
"branch_name": {
|
384
|
+
"type": "string",
|
385
|
+
"description": "Name of branch to checkout. This must be an existing branch in the repository. "
|
386
|
+
"Examples: 'main', 'develop', 'feature/user-authentication'. The branch must exist "
|
387
|
+
"in the repository or the command will fail."
|
388
|
+
}
|
389
|
+
},
|
390
|
+
"required": ["repo_path", "branch_name"]
|
391
|
+
}
|
392
|
+
}
|
393
|
+
|
394
|
+
def git_show_tool():
|
395
|
+
return {
|
396
|
+
"name": "git_show",
|
397
|
+
"description": "Shows the contents of a specific commit. "
|
398
|
+
"WHEN TO USE: When you need to examine the exact changes introduced by a particular commit, understand "
|
399
|
+
"what was modified in a specific revision, or analyze the details of historical changes. Useful for code "
|
400
|
+
"reviews, understanding the implementation of a feature, or troubleshooting when a bug was introduced. "
|
401
|
+
"WHEN NOT TO USE: When the repository has no commits yet, when you only need a list of commits without "
|
402
|
+
"details (use git_log instead), or when you want to compare branches or arbitrary revisions (use "
|
403
|
+
"git_diff instead). "
|
404
|
+
"RETURNS: Detailed information about the specified commit including commit hash, author, date, commit message, "
|
405
|
+
"and a unified diff showing all changes introduced by that commit. For the first commit in a repository, "
|
406
|
+
"shows the complete file contents. Repository must be within the allowed directory.",
|
407
|
+
"inputSchema": {
|
408
|
+
"type": "object",
|
409
|
+
"properties": {
|
410
|
+
"repo_path": {
|
411
|
+
"type": "string",
|
412
|
+
"description": "Path to git repository. This must be a directory containing a valid Git repository. "
|
413
|
+
"Examples: '.', 'my-project', '/absolute/path/to/repo'. Both absolute and relative paths "
|
414
|
+
"are supported, but must be within the allowed workspace."
|
415
|
+
},
|
416
|
+
"revision": {
|
417
|
+
"type": "string",
|
418
|
+
"description": "The revision to show details for. This can be a commit hash (full or abbreviated), branch name, "
|
419
|
+
"tag, or reference like HEAD~1. Examples: 'a1b2c3d' (commit hash), 'main' (branch), 'v1.0' (tag), "
|
420
|
+
"'HEAD~3' (third commit before current HEAD)."
|
421
|
+
}
|
422
|
+
},
|
423
|
+
"required": ["repo_path", "revision"]
|
424
|
+
}
|
425
|
+
}
|
426
|
+
|
427
|
+
async def handle_git_status(arguments: dict) -> List[TextContent]:
|
428
|
+
"""Handle getting git repository status."""
|
429
|
+
repo = _get_repo(arguments["repo_path"])
|
430
|
+
try:
|
431
|
+
status = repo.git.status()
|
432
|
+
return [TextContent(
|
433
|
+
type="text",
|
434
|
+
text=f"Repository status:\n{status}"
|
435
|
+
)]
|
436
|
+
except Exception as e:
|
437
|
+
raise ValueError(f"Error getting repository status at '{repo.working_dir}': {str(e)}")
|
438
|
+
|
439
|
+
async def handle_git_diff_unstaged(arguments: dict) -> List[TextContent]:
|
440
|
+
"""Handle getting unstaged changes."""
|
441
|
+
repo = _get_repo(arguments["repo_path"])
|
442
|
+
try:
|
443
|
+
# For new repos without commits, show diff of staged files
|
444
|
+
if not repo.head.is_valid():
|
445
|
+
# Get the diff against an empty tree
|
446
|
+
diff = repo.git.diff("--no-index", "/dev/null", repo.working_dir)
|
447
|
+
else:
|
448
|
+
diff = repo.git.diff()
|
449
|
+
|
450
|
+
if not diff:
|
451
|
+
return [TextContent(
|
452
|
+
type="text",
|
453
|
+
text="No unstaged changes found."
|
454
|
+
)]
|
455
|
+
return [TextContent(
|
456
|
+
type="text",
|
457
|
+
text=f"Unstaged changes:\n{diff}"
|
458
|
+
)]
|
459
|
+
except Exception as e:
|
460
|
+
raise ValueError(f"Error getting unstaged changes: {str(e)}")
|
461
|
+
|
462
|
+
async def handle_git_diff_staged(arguments: dict) -> List[TextContent]:
|
463
|
+
"""Handle getting staged changes."""
|
464
|
+
repo = _get_repo(arguments["repo_path"])
|
465
|
+
try:
|
466
|
+
# For new repos without commits, show all staged files
|
467
|
+
if not repo.head.is_valid():
|
468
|
+
if repo.index.entries:
|
469
|
+
diff = repo.git.diff("--cached", "--no-index", "/dev/null", "--")
|
470
|
+
else:
|
471
|
+
diff = ""
|
472
|
+
else:
|
473
|
+
diff = repo.git.diff("--cached")
|
474
|
+
if not diff:
|
475
|
+
return [TextContent(
|
476
|
+
type="text",
|
477
|
+
text="No staged changes found."
|
478
|
+
)]
|
479
|
+
return [TextContent(
|
480
|
+
type="text",
|
481
|
+
text=f"Staged changes:\n{diff}"
|
482
|
+
)]
|
483
|
+
except Exception as e:
|
484
|
+
raise ValueError(f"Error getting staged changes at '{repo.working_dir}': {str(e)}")
|
485
|
+
|
486
|
+
async def handle_git_diff(arguments: dict) -> List[TextContent]:
|
487
|
+
"""Handle getting diff between branches or commits."""
|
488
|
+
repo = _get_repo(arguments["repo_path"])
|
489
|
+
target = arguments["target"]
|
490
|
+
try:
|
491
|
+
# Check if repository has any commits
|
492
|
+
if not repo.head.is_valid():
|
493
|
+
raise ValueError(f"Cannot diff against '{target}' in repository at '{repo.working_dir}': No commits exist yet")
|
494
|
+
else:
|
495
|
+
diff = repo.git.diff(target)
|
496
|
+
if not diff:
|
497
|
+
return [TextContent(
|
498
|
+
type="text",
|
499
|
+
text=f"No differences found with {target}."
|
500
|
+
)]
|
501
|
+
return [TextContent(
|
502
|
+
type="text",
|
503
|
+
text=f"Diff with {target}:\n{diff}"
|
504
|
+
)]
|
505
|
+
except Exception as e:
|
506
|
+
raise ValueError(f"Error getting diff at '{repo.working_dir}': {str(e)}")
|
507
|
+
|
508
|
+
async def handle_git_commit(arguments: dict) -> List[TextContent]:
|
509
|
+
"""Handle committing changes."""
|
510
|
+
repo = _get_repo(arguments["repo_path"])
|
511
|
+
message = arguments["message"]
|
512
|
+
try:
|
513
|
+
# Check if this is the first commit
|
514
|
+
is_initial_commit = not repo.head.is_valid()
|
515
|
+
|
516
|
+
if not is_initial_commit:
|
517
|
+
# For non-initial commits, check if there are staged changes
|
518
|
+
if not repo.index.diff("HEAD"):
|
519
|
+
return [TextContent(
|
520
|
+
type="text",
|
521
|
+
text="No changes staged for commit."
|
522
|
+
)]
|
523
|
+
elif not repo.index.entries:
|
524
|
+
return [TextContent(type="text", text="No files staged for initial commit.")]
|
525
|
+
|
526
|
+
# Perform the commit
|
527
|
+
commit = repo.index.commit(message)
|
528
|
+
return [TextContent(
|
529
|
+
type="text",
|
530
|
+
text=f"Changes committed successfully with hash {commit.hexsha}"
|
531
|
+
)]
|
532
|
+
except Exception as e:
|
533
|
+
raise ValueError(f"Error committing changes at '{repo.working_dir}': {str(e)}")
|
534
|
+
|
535
|
+
async def handle_git_add(arguments: dict) -> List[TextContent]:
|
536
|
+
"""Handle staging files."""
|
537
|
+
repo = _get_repo(arguments["repo_path"])
|
538
|
+
files = arguments["files"]
|
539
|
+
try:
|
540
|
+
repo.index.add(files)
|
541
|
+
return [TextContent(
|
542
|
+
type="text",
|
543
|
+
text=f"Successfully staged the following files:\n{', '.join(files)}"
|
544
|
+
)]
|
545
|
+
except Exception as e:
|
546
|
+
raise ValueError(f"Error staging files at '{repo.working_dir}': {str(e)}")
|
547
|
+
|
548
|
+
async def handle_git_reset(arguments: dict) -> List[TextContent]:
|
549
|
+
"""Handle unstaging all changes."""
|
550
|
+
repo = _get_repo(arguments["repo_path"])
|
551
|
+
try:
|
552
|
+
# Check if this is a new repository without any commits
|
553
|
+
if not repo.head.is_valid():
|
554
|
+
# For new repos, just remove all from index
|
555
|
+
repo.index.remove(repo.index.entries.keys())
|
556
|
+
repo.index.write()
|
557
|
+
return [TextContent(
|
558
|
+
type="text",
|
559
|
+
text="Successfully unstaged all changes (new repository)"
|
560
|
+
)]
|
561
|
+
else:
|
562
|
+
repo.index.reset() # Normal reset for repositories with commits
|
563
|
+
return [TextContent(
|
564
|
+
type="text",
|
565
|
+
text="Successfully unstaged all changes"
|
566
|
+
)]
|
567
|
+
except Exception as e:
|
568
|
+
raise ValueError(f"Error unstaging changes at '{repo.working_dir}': {str(e)}")
|
569
|
+
|
570
|
+
async def handle_git_log(arguments: dict) -> List[TextContent]:
|
571
|
+
"""Handle showing commit logs."""
|
572
|
+
repo = _get_repo(arguments["repo_path"])
|
573
|
+
max_count = arguments.get("max_count", 10)
|
574
|
+
try:
|
575
|
+
# Check if repository has any commits
|
576
|
+
if not repo.head.is_valid():
|
577
|
+
return [TextContent(
|
578
|
+
type="text",
|
579
|
+
text="No commits yet - this is a new repository."
|
580
|
+
)]
|
581
|
+
commits = list(repo.iter_commits(max_count=max_count))
|
582
|
+
if not commits:
|
583
|
+
return [TextContent(
|
584
|
+
type="text",
|
585
|
+
text="No commits found in repository."
|
586
|
+
)]
|
587
|
+
|
588
|
+
log_entries = []
|
589
|
+
for commit in commits:
|
590
|
+
log_entries.append(
|
591
|
+
f"Commit: {commit.hexsha}\n"
|
592
|
+
f"Author: {commit.author}\n"
|
593
|
+
f"Date: {commit.authored_datetime}\n"
|
594
|
+
f"Message: {commit.message}\n"
|
595
|
+
)
|
596
|
+
|
597
|
+
return [TextContent(
|
598
|
+
type="text",
|
599
|
+
text="Commit history:\n" + "\n".join(log_entries)
|
600
|
+
)]
|
601
|
+
except Exception as e:
|
602
|
+
raise ValueError(f"Error getting commit logs at '{repo.working_dir}': {str(e)}")
|
603
|
+
|
604
|
+
async def handle_git_create_branch(arguments: dict) -> List[TextContent]:
|
605
|
+
"""Handle creating a new branch."""
|
606
|
+
repo = _get_repo(arguments["repo_path"])
|
607
|
+
branch_name = arguments["branch_name"]
|
608
|
+
base_branch = arguments.get("base_branch")
|
609
|
+
|
610
|
+
# Check if repository has any commits
|
611
|
+
if not repo.head.is_valid():
|
612
|
+
return [TextContent(
|
613
|
+
type="text",
|
614
|
+
text=f"Cannot create branch '{branch_name}' - no commits exist yet. Make an initial commit first."
|
615
|
+
)]
|
616
|
+
|
617
|
+
try:
|
618
|
+
if base_branch:
|
619
|
+
base = repo.refs[base_branch]
|
620
|
+
else: # We already checked head.is_valid() above
|
621
|
+
base = repo.active_branch
|
622
|
+
|
623
|
+
repo.create_head(branch_name, base)
|
624
|
+
return [TextContent(
|
625
|
+
type="text",
|
626
|
+
text=f"Created branch '{branch_name}' from '{base.name}'"
|
627
|
+
)]
|
628
|
+
except Exception as e:
|
629
|
+
raise ValueError(f"Error creating branch at '{repo.working_dir}': {str(e)}")
|
630
|
+
|
631
|
+
async def handle_git_checkout(arguments: dict) -> List[TextContent]:
|
632
|
+
"""Handle switching branches."""
|
633
|
+
repo = _get_repo(arguments["repo_path"])
|
634
|
+
branch_name = arguments["branch_name"]
|
635
|
+
try:
|
636
|
+
# Check if repository has any commits
|
637
|
+
if not repo.head.is_valid():
|
638
|
+
return [TextContent(
|
639
|
+
type="text",
|
640
|
+
text=f"Cannot checkout branch '{branch_name}' - no commits exist yet. Make an initial commit first."
|
641
|
+
)]
|
642
|
+
|
643
|
+
repo.git.checkout(branch_name)
|
644
|
+
return [TextContent(
|
645
|
+
type="text",
|
646
|
+
text=f"Successfully switched to branch '{branch_name}'"
|
647
|
+
)]
|
648
|
+
except Exception as e:
|
649
|
+
raise ValueError(f"Error switching branches at '{repo.working_dir}': {str(e)}")
|
650
|
+
|
651
|
+
async def handle_git_show(arguments: dict) -> List[TextContent]:
|
652
|
+
"""Handle showing commit contents."""
|
653
|
+
repo = _get_repo(arguments["repo_path"])
|
654
|
+
revision = arguments["revision"]
|
655
|
+
try:
|
656
|
+
# Check if repository has any commits
|
657
|
+
if not repo.head.is_valid():
|
658
|
+
return [TextContent(
|
659
|
+
type="text",
|
660
|
+
text=f"Cannot show revision '{revision}' - no commits exist yet."
|
661
|
+
)]
|
662
|
+
|
663
|
+
commit = repo.commit(revision)
|
664
|
+
output = [
|
665
|
+
f"Commit: {commit.hexsha}\n"
|
666
|
+
f"Author: {commit.author}\n"
|
667
|
+
f"Date: {commit.authored_datetime}\n"
|
668
|
+
f"Message: {commit.message}\n"
|
669
|
+
]
|
670
|
+
|
671
|
+
# Get the diff
|
672
|
+
if commit.parents:
|
673
|
+
parent = commit.parents[0]
|
674
|
+
diff = parent.diff(commit, create_patch=True)
|
675
|
+
else:
|
676
|
+
diff = commit.diff(git.NULL_TREE, create_patch=True)
|
677
|
+
|
678
|
+
for d in diff:
|
679
|
+
output.append(f"\n--- {d.a_path}\n+++ {d.b_path}\n")
|
680
|
+
output.append(d.diff.decode('utf-8'))
|
681
|
+
|
682
|
+
return [TextContent(
|
683
|
+
type="text",
|
684
|
+
text="".join(output)
|
685
|
+
)]
|
686
|
+
except Exception as e:
|
687
|
+
raise ValueError(f"Error showing commit at '{repo.working_dir}': {str(e)}")
|