@cyanheads/git-mcp-server 2.1.0 → 2.1.2

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.
Files changed (97) hide show
  1. package/README.md +8 -11
  2. package/dist/config/index.js +7 -7
  3. package/dist/index.js +35 -21
  4. package/dist/mcp-server/server.js +72 -56
  5. package/dist/mcp-server/tools/gitAdd/index.js +1 -1
  6. package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
  7. package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
  8. package/dist/mcp-server/tools/gitBranch/index.js +1 -1
  9. package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
  10. package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
  11. package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
  12. package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
  13. package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
  14. package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
  15. package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
  16. package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
  17. package/dist/mcp-server/tools/gitClean/index.js +1 -1
  18. package/dist/mcp-server/tools/gitClean/logic.js +93 -41
  19. package/dist/mcp-server/tools/gitClean/registration.js +19 -16
  20. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
  21. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
  22. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
  23. package/dist/mcp-server/tools/gitClone/index.js +1 -1
  24. package/dist/mcp-server/tools/gitClone/logic.js +89 -30
  25. package/dist/mcp-server/tools/gitClone/registration.js +15 -12
  26. package/dist/mcp-server/tools/gitCommit/index.js +1 -1
  27. package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
  28. package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
  29. package/dist/mcp-server/tools/gitDiff/index.js +1 -1
  30. package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
  31. package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
  32. package/dist/mcp-server/tools/gitFetch/index.js +1 -1
  33. package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
  34. package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
  35. package/dist/mcp-server/tools/gitInit/index.js +1 -1
  36. package/dist/mcp-server/tools/gitInit/logic.js +88 -34
  37. package/dist/mcp-server/tools/gitInit/registration.js +32 -18
  38. package/dist/mcp-server/tools/gitLog/index.js +1 -1
  39. package/dist/mcp-server/tools/gitLog/logic.js +133 -47
  40. package/dist/mcp-server/tools/gitLog/registration.js +16 -14
  41. package/dist/mcp-server/tools/gitMerge/index.js +1 -1
  42. package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
  43. package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
  44. package/dist/mcp-server/tools/gitPull/index.js +1 -1
  45. package/dist/mcp-server/tools/gitPull/logic.js +90 -69
  46. package/dist/mcp-server/tools/gitPull/registration.js +16 -14
  47. package/dist/mcp-server/tools/gitPush/index.js +1 -1
  48. package/dist/mcp-server/tools/gitPush/logic.js +116 -100
  49. package/dist/mcp-server/tools/gitPush/registration.js +16 -14
  50. package/dist/mcp-server/tools/gitRebase/index.js +1 -1
  51. package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
  52. package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
  53. package/dist/mcp-server/tools/gitRemote/index.js +1 -1
  54. package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
  55. package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
  56. package/dist/mcp-server/tools/gitReset/index.js +1 -1
  57. package/dist/mcp-server/tools/gitReset/logic.js +65 -37
  58. package/dist/mcp-server/tools/gitReset/registration.js +14 -12
  59. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
  60. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
  61. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
  62. package/dist/mcp-server/tools/gitShow/index.js +1 -1
  63. package/dist/mcp-server/tools/gitShow/logic.js +78 -35
  64. package/dist/mcp-server/tools/gitShow/registration.js +17 -12
  65. package/dist/mcp-server/tools/gitStash/index.js +1 -1
  66. package/dist/mcp-server/tools/gitStash/logic.js +143 -58
  67. package/dist/mcp-server/tools/gitStash/registration.js +19 -12
  68. package/dist/mcp-server/tools/gitStatus/index.js +1 -1
  69. package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
  70. package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
  71. package/dist/mcp-server/tools/gitTag/index.js +1 -1
  72. package/dist/mcp-server/tools/gitTag/logic.js +124 -51
  73. package/dist/mcp-server/tools/gitTag/registration.js +14 -11
  74. package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
  75. package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
  76. package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
  77. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
  78. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
  79. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
  80. package/dist/mcp-server/transports/httpTransport.js +187 -79
  81. package/dist/mcp-server/transports/stdioTransport.js +14 -8
  82. package/dist/types-global/errors.js +9 -4
  83. package/dist/utils/index.js +4 -4
  84. package/dist/utils/internal/errorHandler.js +62 -40
  85. package/dist/utils/internal/index.js +3 -3
  86. package/dist/utils/internal/logger.js +97 -54
  87. package/dist/utils/internal/requestContext.js +7 -5
  88. package/dist/utils/metrics/index.js +1 -1
  89. package/dist/utils/metrics/tokenCounter.js +18 -14
  90. package/dist/utils/parsing/dateParser.js +5 -5
  91. package/dist/utils/parsing/index.js +2 -2
  92. package/dist/utils/parsing/jsonParser.js +20 -11
  93. package/dist/utils/security/idGenerator.js +8 -10
  94. package/dist/utils/security/index.js +3 -3
  95. package/dist/utils/security/rateLimiter.js +16 -14
  96. package/dist/utils/security/sanitization.js +139 -82
  97. package/package.json +45 -23
@@ -1,26 +1,49 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import { z } from 'zod';
4
- import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { z } from "zod";
4
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
5
5
  // Import utils from barrel (logger from ../utils/internal/logger.js)
6
- import { logger } from '../../../utils/index.js';
6
+ import { logger } from "../../../utils/index.js";
7
7
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
8
- import { sanitization } from '../../../utils/index.js';
8
+ import { sanitization } from "../../../utils/index.js";
9
9
  // Import config to check signing flag
10
- import { config } from '../../../config/index.js';
10
+ import { config } from "../../../config/index.js";
11
11
  const execAsync = promisify(exec);
12
12
  // Define the input schema for the git_commit tool using Zod
13
13
  export const GitCommitInputSchema = z.object({
14
- path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
15
- message: z.string().min(1).describe('Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`'),
16
- author: z.object({
17
- name: z.string().describe('Author name for the commit'),
18
- email: z.string().email().describe('Author email for the commit'),
19
- }).optional().describe('Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches).'),
20
- allowEmpty: z.boolean().default(false).describe('Allow creating empty commits'),
21
- amend: z.boolean().default(false).describe('Amend the previous commit instead of creating a new one'),
22
- forceUnsignedOnFailure: z.boolean().default(false).describe('If true and signing is enabled but fails, attempt the commit without signing instead of failing.'),
23
- filesToStage: z.array(z.string().min(1)).optional().describe('Optional array of specific file paths (relative to the repository root) to stage automatically before committing. If provided, only these files will be staged.'),
14
+ path: z
15
+ .string()
16
+ .min(1)
17
+ .optional()
18
+ .default(".")
19
+ .describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
20
+ message: z
21
+ .string()
22
+ .min(1)
23
+ .describe("Commit message. Follow Conventional Commits format: `type(scope): subject`. Example: `feat(api): add user signup endpoint`"),
24
+ author: z
25
+ .object({
26
+ name: z.string().describe("Author name for the commit"),
27
+ email: z.string().email().describe("Author email for the commit"),
28
+ })
29
+ .optional()
30
+ .describe("Overrides the commit author information (name and email). Use only when necessary (e.g., applying external patches)."),
31
+ allowEmpty: z
32
+ .boolean()
33
+ .default(false)
34
+ .describe("Allow creating empty commits"),
35
+ amend: z
36
+ .boolean()
37
+ .default(false)
38
+ .describe("Amend the previous commit instead of creating a new one"),
39
+ forceUnsignedOnFailure: z
40
+ .boolean()
41
+ .default(false)
42
+ .describe("If true and signing is enabled but fails, attempt the commit without signing instead of failing."),
43
+ filesToStage: z
44
+ .array(z.string().min(1))
45
+ .optional()
46
+ .describe("Optional array of specific file paths (relative to the repository root) to stage automatically before committing. If provided, only these files will be staged."),
24
47
  });
25
48
  /**
26
49
  * Executes the 'git commit' command and returns structured JSON output.
@@ -30,16 +53,18 @@ export const GitCommitInputSchema = z.object({
30
53
  * @returns {Promise<GitCommitResult>} A promise that resolves with the structured commit result.
31
54
  * @throws {McpError} Throws an McpError if path resolution or validation fails, or if the git command fails unexpectedly.
32
55
  */
33
- export async function commitGitChanges(input, context // Add getter to context
34
- ) {
35
- const operation = 'commitGitChanges';
56
+ export async function commitGitChanges(input, context) {
57
+ const operation = "commitGitChanges";
36
58
  logger.debug(`Executing ${operation}`, { ...context, input });
37
59
  let targetPath;
38
60
  try {
39
61
  // Resolve the target path
40
- if (input.path && input.path !== '.') {
62
+ if (input.path && input.path !== ".") {
41
63
  targetPath = input.path;
42
- logger.debug(`Using provided path: ${targetPath}`, { ...context, operation });
64
+ logger.debug(`Using provided path: ${targetPath}`, {
65
+ ...context,
66
+ operation,
67
+ });
43
68
  }
44
69
  else {
45
70
  const workingDir = context.getWorkingDirectory();
@@ -47,15 +72,29 @@ export async function commitGitChanges(input, context // Add getter to context
47
72
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No path provided and no working directory set for the session.", { context, operation });
48
73
  }
49
74
  targetPath = workingDir;
50
- logger.debug(`Using session working directory: ${targetPath}`, { ...context, operation, sessionId: context.sessionId });
75
+ logger.debug(`Using session working directory: ${targetPath}`, {
76
+ ...context,
77
+ operation,
78
+ sessionId: context.sessionId,
79
+ });
51
80
  }
52
81
  // Sanitize the resolved path
53
- const sanitizedPathInfo = sanitization.sanitizePath(targetPath, { allowAbsolute: true });
54
- logger.debug('Sanitized path', { ...context, operation, sanitizedPathInfo });
82
+ const sanitizedPathInfo = sanitization.sanitizePath(targetPath, {
83
+ allowAbsolute: true,
84
+ });
85
+ logger.debug("Sanitized path", {
86
+ ...context,
87
+ operation,
88
+ sanitizedPathInfo,
89
+ });
55
90
  targetPath = sanitizedPathInfo.sanitizedPath; // Use the sanitized path going forward
56
91
  }
57
92
  catch (error) {
58
- logger.error('Path resolution or sanitization failed', { ...context, operation, error });
93
+ logger.error("Path resolution or sanitization failed", {
94
+ ...context,
95
+ operation,
96
+ error,
97
+ });
59
98
  if (error instanceof McpError) {
60
99
  throw error;
61
100
  }
@@ -64,18 +103,30 @@ export async function commitGitChanges(input, context // Add getter to context
64
103
  try {
65
104
  // --- Stage specific files if requested ---
66
105
  if (input.filesToStage && input.filesToStage.length > 0) {
67
- logger.debug(`Attempting to stage specific files: ${input.filesToStage.join(', ')}`, { ...context, operation });
106
+ logger.debug(`Attempting to stage specific files: ${input.filesToStage.join(", ")}`, { ...context, operation });
68
107
  try {
69
108
  // Correctly pass targetPath as rootDir in options object
70
- const sanitizedFiles = input.filesToStage.map(file => sanitization.sanitizePath(file, { rootDir: targetPath }).sanitizedPath); // Sanitize relative to repo root
71
- const filesToAddString = sanitizedFiles.map(file => `"${file}"`).join(' '); // Quote paths for safety
109
+ const sanitizedFiles = input.filesToStage.map((file) => sanitization.sanitizePath(file, { rootDir: targetPath })
110
+ .sanitizedPath); // Sanitize relative to repo root
111
+ const filesToAddString = sanitizedFiles
112
+ .map((file) => `"${file}"`)
113
+ .join(" "); // Quote paths for safety
72
114
  const addCommand = `git -C "${targetPath}" add -- ${filesToAddString}`;
73
- logger.debug(`Executing git add command: ${addCommand}`, { ...context, operation });
115
+ logger.debug(`Executing git add command: ${addCommand}`, {
116
+ ...context,
117
+ operation,
118
+ });
74
119
  await execAsync(addCommand);
75
- logger.info(`Successfully staged specified files: ${sanitizedFiles.join(', ')}`, { ...context, operation });
120
+ logger.info(`Successfully staged specified files: ${sanitizedFiles.join(", ")}`, { ...context, operation });
76
121
  }
77
122
  catch (addError) {
78
- logger.error('Failed to stage specified files', { ...context, operation, files: input.filesToStage, error: addError.message, stderr: addError.stderr });
123
+ logger.error("Failed to stage specified files", {
124
+ ...context,
125
+ operation,
126
+ files: input.filesToStage,
127
+ error: addError.message,
128
+ stderr: addError.stderr,
129
+ });
79
130
  throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to stage files before commit: ${addError.stderr || addError.message}`, { context, operation, originalError: addError });
80
131
  }
81
132
  }
@@ -83,16 +134,20 @@ export async function commitGitChanges(input, context // Add getter to context
83
134
  // Escape message for shell safety
84
135
  const escapeShellArg = (arg) => {
85
136
  // Escape backslashes first, then other special chars
86
- return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
137
+ return arg
138
+ .replace(/\\/g, "\\\\")
139
+ .replace(/"/g, '\\"')
140
+ .replace(/`/g, "\\`")
141
+ .replace(/\$/g, "\\$");
87
142
  };
88
143
  const escapedMessage = escapeShellArg(input.message);
89
144
  // Construct the git commit command using the resolved targetPath
90
145
  let command = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
91
146
  if (input.allowEmpty) {
92
- command += ' --allow-empty';
147
+ command += " --allow-empty";
93
148
  }
94
149
  if (input.amend) {
95
- command += ' --amend --no-edit';
150
+ command += " --amend --no-edit";
96
151
  }
97
152
  if (input.author) {
98
153
  // Escape author details as well
@@ -102,16 +157,19 @@ export async function commitGitChanges(input, context // Add getter to context
102
157
  command = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
103
158
  }
104
159
  // Append common flags (ensure they are appended to the potentially modified command from author block)
105
- if (input.allowEmpty && !command.includes(' --allow-empty'))
106
- command += ' --allow-empty';
107
- if (input.amend && !command.includes(' --amend'))
108
- command += ' --amend --no-edit'; // Avoid double adding if author block modified command
160
+ if (input.allowEmpty && !command.includes(" --allow-empty"))
161
+ command += " --allow-empty";
162
+ if (input.amend && !command.includes(" --amend"))
163
+ command += " --amend --no-edit"; // Avoid double adding if author block modified command
109
164
  // Append signing flag if configured via GIT_SIGN_COMMITS env var
110
165
  if (config.gitSignCommits) {
111
- command += ' -S'; // Add signing flag (-S)
112
- logger.info('Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.', { ...context, operation });
166
+ command += " -S"; // Add signing flag (-S)
167
+ logger.info("Signing enabled via GIT_SIGN_COMMITS=true, adding -S flag.", { ...context, operation });
113
168
  }
114
- logger.debug(`Executing initial command attempt: ${command}`, { ...context, operation });
169
+ logger.debug(`Executing initial command attempt: ${command}`, {
170
+ ...context,
171
+ operation,
172
+ });
115
173
  let stdout;
116
174
  let stderr;
117
175
  let commitResult;
@@ -122,29 +180,34 @@ export async function commitGitChanges(input, context // Add getter to context
122
180
  stderr = execResult.stderr;
123
181
  }
124
182
  catch (error) {
125
- const initialErrorMessage = error.stderr || error.message || '';
126
- const isSigningError = initialErrorMessage.includes('gpg failed to sign') || initialErrorMessage.includes('signing failed');
183
+ const initialErrorMessage = error.stderr || error.message || "";
184
+ const isSigningError = initialErrorMessage.includes("gpg failed to sign") ||
185
+ initialErrorMessage.includes("signing failed");
127
186
  if (isSigningError && input.forceUnsignedOnFailure) {
128
- logger.warning('Initial commit attempt failed due to signing error. Retrying without signing as forceUnsignedOnFailure=true.', { ...context, operation, initialError: initialErrorMessage });
187
+ logger.warning("Initial commit attempt failed due to signing error. Retrying without signing as forceUnsignedOnFailure=true.", { ...context, operation, initialError: initialErrorMessage });
129
188
  // Construct command *without* -S flag, using escaped message/author
130
189
  const escapeShellArg = (arg) => {
131
- return arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
190
+ return arg
191
+ .replace(/\\/g, "\\\\")
192
+ .replace(/"/g, '\\"')
193
+ .replace(/`/g, "\\`")
194
+ .replace(/\$/g, "\\$");
132
195
  };
133
196
  const escapedMessage = escapeShellArg(input.message);
134
197
  let unsignedCommand = `git -C "${targetPath}" commit -m "${escapedMessage}"`;
135
198
  if (input.allowEmpty)
136
- unsignedCommand += ' --allow-empty';
199
+ unsignedCommand += " --allow-empty";
137
200
  if (input.amend)
138
- unsignedCommand += ' --amend --no-edit';
201
+ unsignedCommand += " --amend --no-edit";
139
202
  if (input.author) {
140
203
  const escapedAuthorName = escapeShellArg(input.author.name);
141
204
  const escapedAuthorEmail = escapeShellArg(input.author.email);
142
205
  unsignedCommand = `git -C "${targetPath}" -c user.name="${escapedAuthorName}" -c user.email="${escapedAuthorEmail}" commit -m "${escapedMessage}"`;
143
206
  // Re-append common flags if author block overwrote command
144
- if (input.allowEmpty && !unsignedCommand.includes(' --allow-empty'))
145
- unsignedCommand += ' --allow-empty';
146
- if (input.amend && !unsignedCommand.includes(' --amend'))
147
- unsignedCommand += ' --amend --no-edit';
207
+ if (input.allowEmpty && !unsignedCommand.includes(" --allow-empty"))
208
+ unsignedCommand += " --allow-empty";
209
+ if (input.amend && !unsignedCommand.includes(" --amend"))
210
+ unsignedCommand += " --amend --no-edit";
148
211
  }
149
212
  logger.debug(`Executing unsigned fallback command: ${unsignedCommand}`, { ...context, operation });
150
213
  try {
@@ -156,12 +219,17 @@ export async function commitGitChanges(input, context // Add getter to context
156
219
  commitResult = {
157
220
  success: true,
158
221
  statusMessage: `Commit successful (unsigned, signing failed): ${stdout.trim()}`, // Default message, hash parsed below
159
- commitHash: undefined // Will be parsed below
222
+ commitHash: undefined, // Will be parsed below
160
223
  };
161
224
  }
162
225
  catch (fallbackError) {
163
226
  // If the unsigned commit *also* fails, re-throw that error
164
- logger.error('Unsigned fallback commit attempt also failed.', { ...context, operation, fallbackError: fallbackError.message, stderr: fallbackError.stderr });
227
+ logger.error("Unsigned fallback commit attempt also failed.", {
228
+ ...context,
229
+ operation,
230
+ fallbackError: fallbackError.message,
231
+ stderr: fallbackError.stderr,
232
+ });
165
233
  throw fallbackError; // Re-throw the error from the unsigned attempt
166
234
  }
167
235
  }
@@ -172,15 +240,23 @@ export async function commitGitChanges(input, context // Add getter to context
172
240
  }
173
241
  // Process result (either from initial attempt or fallback)
174
242
  // Check stderr first for common non-error messages
175
- if (stderr && !commitResult) { // Don't overwrite fallback message if stderr also exists
176
- if (stderr.includes('nothing to commit, working tree clean') || stderr.includes('no changes added to commit')) {
177
- const msg = stderr.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
243
+ if (stderr && !commitResult) {
244
+ // Don't overwrite fallback message if stderr also exists
245
+ if (stderr.includes("nothing to commit, working tree clean") ||
246
+ stderr.includes("no changes added to commit")) {
247
+ const msg = stderr.includes("nothing to commit")
248
+ ? "Nothing to commit, working tree clean."
249
+ : "No changes added to commit.";
178
250
  logger.info(msg, { ...context, operation, path: targetPath });
179
251
  // Use statusMessage
180
252
  return { success: true, statusMessage: msg, nothingToCommit: true };
181
253
  }
182
254
  // Log other stderr as warning but continue, as commit might still succeed
183
- logger.warning(`Git commit command produced stderr`, { ...context, operation, stderr });
255
+ logger.warning(`Git commit command produced stderr`, {
256
+ ...context,
257
+ operation,
258
+ stderr,
259
+ });
184
260
  }
185
261
  // Extract commit hash (more robustly)
186
262
  let commitHash = undefined;
@@ -190,51 +266,97 @@ export async function commitGitChanges(input, context // Add getter to context
190
266
  }
191
267
  else {
192
268
  // Fallback parsing if needed, or rely on success message
193
- logger.warning('Could not parse commit hash from stdout', { ...context, operation, stdout });
269
+ logger.warning("Could not parse commit hash from stdout", {
270
+ ...context,
271
+ operation,
272
+ stdout,
273
+ });
194
274
  }
195
275
  // Use statusMessage, potentially using the one set during fallback
196
- const finalStatusMsg = commitResult?.statusMessage || (commitHash
197
- ? `Commit successful: ${commitHash}`
198
- : `Commit successful (stdout: ${stdout.trim()})`);
276
+ const finalStatusMsg = commitResult?.statusMessage ||
277
+ (commitHash
278
+ ? `Commit successful: ${commitHash}`
279
+ : `Commit successful (stdout: ${stdout.trim()})`);
199
280
  let committedFiles = [];
200
281
  if (commitHash) {
201
282
  try {
202
283
  // Get the list of files included in this specific commit
203
284
  const showCommand = `git -C "${targetPath}" show --pretty="" --name-only ${commitHash}`;
204
- logger.debug(`Executing git show command: ${showCommand}`, { ...context, operation });
285
+ logger.debug(`Executing git show command: ${showCommand}`, {
286
+ ...context,
287
+ operation,
288
+ });
205
289
  const { stdout: showStdout } = await execAsync(showCommand);
206
- committedFiles = showStdout.trim().split('\n').filter(Boolean); // Split by newline, remove empty lines
207
- logger.debug(`Retrieved committed files list for ${commitHash}`, { ...context, operation, count: committedFiles.length });
290
+ committedFiles = showStdout.trim().split("\n").filter(Boolean); // Split by newline, remove empty lines
291
+ logger.debug(`Retrieved committed files list for ${commitHash}`, {
292
+ ...context,
293
+ operation,
294
+ count: committedFiles.length,
295
+ });
208
296
  }
209
297
  catch (showError) {
210
298
  // Log a warning but don't fail the overall operation if we can't get the file list
211
- logger.warning('Failed to retrieve committed files list', { ...context, operation, commitHash, error: showError.message, stderr: showError.stderr });
299
+ logger.warning("Failed to retrieve committed files list", {
300
+ ...context,
301
+ operation,
302
+ commitHash,
303
+ error: showError.message,
304
+ stderr: showError.stderr,
305
+ });
212
306
  }
213
307
  }
214
- logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath, commitHash, signed: !commitResult, committedFilesCount: committedFiles.length }); // Log if it was signed (not fallback)
308
+ const successMessage = `Commit successful: ${commitHash}`;
309
+ logger.info(successMessage, {
310
+ ...context,
311
+ operation,
312
+ path: targetPath,
313
+ commitHash,
314
+ signed: !commitResult, // Log if it was signed (not fallback)
315
+ committedFilesCount: committedFiles.length,
316
+ });
215
317
  return {
216
318
  success: true,
217
319
  statusMessage: finalStatusMsg, // Use potentially modified message
218
320
  commitHash: commitHash,
219
321
  commitMessage: input.message, // Include the original commit message
220
- committedFiles: committedFiles // Include the list of files
322
+ committedFiles: committedFiles, // Include the list of files
221
323
  };
222
324
  }
223
- catch (error) { // This catch block now primarily handles non-signing errors or errors from the fallback attempt
224
- logger.error(`Failed to execute git commit command`, { ...context, operation, path: targetPath, error: error.message, stderr: error.stderr });
225
- const errorMessage = error.stderr || error.message || '';
325
+ catch (error) {
326
+ // This catch block now primarily handles non-signing errors or errors from the fallback attempt
327
+ logger.error(`Failed to execute git commit command`, {
328
+ ...context,
329
+ operation,
330
+ path: targetPath,
331
+ error: error.message,
332
+ stderr: error.stderr,
333
+ });
334
+ const errorMessage = error.stderr || error.message || "";
226
335
  // Handle specific error cases first
227
- if (errorMessage.toLowerCase().includes('not a git repository')) {
336
+ if (errorMessage.toLowerCase().includes("not a git repository")) {
228
337
  throw new McpError(BaseErrorCode.NOT_FOUND, `Path is not a Git repository: ${targetPath}`, { context, operation, originalError: error });
229
338
  }
230
- if (errorMessage.includes('nothing to commit') || errorMessage.includes('no changes added to commit')) {
339
+ // Check for pre-commit hook failures before checking for generic conflicts
340
+ if (errorMessage.toLowerCase().includes("pre-commit hook") ||
341
+ errorMessage.toLowerCase().includes("hook failed")) {
342
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Commit failed due to pre-commit hook failure. Details: ${errorMessage}`, { context, operation, originalError: error });
343
+ }
344
+ if (errorMessage.includes("nothing to commit") ||
345
+ errorMessage.includes("no changes added to commit")) {
231
346
  // This might happen if git exits with error despite these messages
232
- const msg = errorMessage.includes('nothing to commit') ? 'Nothing to commit, working tree clean.' : 'No changes added to commit.';
233
- logger.info(msg + ' (caught as error)', { ...context, operation, path: targetPath, errorMessage });
347
+ const msg = errorMessage.includes("nothing to commit")
348
+ ? "Nothing to commit, working tree clean."
349
+ : "No changes added to commit.";
350
+ logger.info(msg + " (caught as error)", {
351
+ ...context,
352
+ operation,
353
+ path: targetPath,
354
+ errorMessage,
355
+ });
234
356
  // Return success=false but indicate the reason using statusMessage
235
357
  return { success: false, statusMessage: msg, nothingToCommit: true };
236
358
  }
237
- if (errorMessage.includes('conflicts')) {
359
+ if (errorMessage.includes("conflicts")) {
238
360
  throw new McpError(BaseErrorCode.CONFLICT, `Commit failed due to unresolved conflicts in ${targetPath}`, { context, operation, originalError: error });
239
361
  }
240
362
  // Generic internal error for other failures
@@ -1,12 +1,12 @@
1
1
  // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
- import { ErrorHandler } from '../../../utils/index.js';
2
+ import { ErrorHandler } from "../../../utils/index.js";
3
3
  // Import utils from barrel (logger from ../utils/internal/logger.js)
4
- import { logger } from '../../../utils/index.js';
4
+ import { logger } from "../../../utils/index.js";
5
5
  // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
- import { requestContextService } from '../../../utils/index.js';
6
+ import { requestContextService } from "../../../utils/index.js";
7
7
  // Import the result type along with the function and input schema
8
- import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
9
- import { commitGitChanges, GitCommitInputSchema } from './logic.js';
8
+ import { BaseErrorCode } from "../../../types-global/errors.js"; // Keep direct import for types-global
9
+ import { commitGitChanges, GitCommitInputSchema, } from "./logic.js";
10
10
  let _getWorkingDirectory;
11
11
  let _getSessionId;
12
12
  /**
@@ -18,9 +18,9 @@ let _getSessionId;
18
18
  export function initializeGitCommitStateAccessors(getWdFn, getSidFn) {
19
19
  _getWorkingDirectory = getWdFn;
20
20
  _getSessionId = getSidFn;
21
- logger.info('State accessors initialized for git_commit tool registration.');
21
+ logger.info("State accessors initialized for git_commit tool registration.");
22
22
  }
23
- const TOOL_NAME = 'git_commit';
23
+ const TOOL_NAME = "git_commit";
24
24
  const TOOL_DESCRIPTION = `Commits staged changes to the Git repository index with a descriptive message. Supports author override, amending, and empty commits. Returns a JSON result.
25
25
 
26
26
  **Commit Message Guidance:**
@@ -45,24 +45,27 @@ Closes #123 (if applicable).
45
45
  - Commit related changes logically. Use the optional \`filesToStage\` parameter to auto-stage specific files before committing.
46
46
  - The \`path\` defaults to the session's working directory unless overridden. If \`GIT_SIGN_COMMITS=true\` is set, commits are signed (\`-S\`), with an optional \`forceUnsignedOnFailure\` fallback.`;
47
47
  /**
48
- * Registers the git_commit tool with the MCP server.
49
- * Uses the high-level server.tool() method for registration, schema validation, and routing.
50
- *
51
- * @param {McpServer} server - The McpServer instance to register the tool with.
52
- * @returns {Promise<void>}
53
- * @throws {Error} If registration fails or state accessors are not initialized.
54
- */
48
+ * Registers the git_commit tool with the MCP server.
49
+ * Uses the high-level server.tool() method for registration, schema validation, and routing.
50
+ *
51
+ * @param {McpServer} server - The McpServer instance to register the tool with.
52
+ * @returns {Promise<void>}
53
+ * @throws {Error} If registration fails or state accessors are not initialized.
54
+ */
55
55
  export const registerGitCommitTool = async (server) => {
56
56
  if (!_getWorkingDirectory || !_getSessionId) {
57
- throw new Error('State accessors for git_commit must be initialized before registration.');
57
+ throw new Error("State accessors for git_commit must be initialized before registration.");
58
58
  }
59
- const operation = 'registerGitCommitTool';
59
+ const operation = "registerGitCommitTool";
60
60
  const context = requestContextService.createRequestContext({ operation });
61
61
  await ErrorHandler.tryCatch(async () => {
62
62
  server.tool(TOOL_NAME, TOOL_DESCRIPTION, GitCommitInputSchema.shape, // Provide the Zod schema shape
63
63
  async (validatedArgs, callContext) => {
64
- const toolOperation = 'tool:git_commit';
65
- const requestContext = requestContextService.createRequestContext({ operation: toolOperation, parentContext: callContext });
64
+ const toolOperation = "tool:git_commit";
65
+ const requestContext = requestContextService.createRequestContext({
66
+ operation: toolOperation,
67
+ parentContext: callContext,
68
+ });
66
69
  const sessionId = _getSessionId(requestContext);
67
70
  const getWorkingDirectoryForSession = () => {
68
71
  return _getWorkingDirectory(sessionId);
@@ -78,10 +81,10 @@ export const registerGitCommitTool = async (server) => {
78
81
  const commitResult = await commitGitChanges(validatedArgs, logicContext);
79
82
  // Format the result as a JSON string within TextContent
80
83
  const resultContent = {
81
- type: 'text',
84
+ type: "text",
82
85
  // Stringify the JSON object for the response content
83
86
  text: JSON.stringify(commitResult, null, 2), // Pretty-print JSON
84
- contentType: 'application/json',
87
+ contentType: "application/json",
85
88
  };
86
89
  // Log based on the success flag in the result
87
90
  if (commitResult.success) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Barrel file for the gitDiff tool.
3
3
  */
4
- export { registerGitDiffTool, initializeGitDiffStateAccessors } from './registration.js';
4
+ export { registerGitDiffTool, initializeGitDiffStateAccessors, } from "./registration.js";
5
5
  // Export types if needed elsewhere, e.g.:
6
6
  // export type { GitDiffInput, GitDiffResult } from './logic.js';