gitx.do 0.0.1

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/durable-object/object-store.d.ts +113 -0
  4. package/dist/durable-object/object-store.d.ts.map +1 -0
  5. package/dist/durable-object/object-store.js +387 -0
  6. package/dist/durable-object/object-store.js.map +1 -0
  7. package/dist/durable-object/schema.d.ts +17 -0
  8. package/dist/durable-object/schema.d.ts.map +1 -0
  9. package/dist/durable-object/schema.js +43 -0
  10. package/dist/durable-object/schema.js.map +1 -0
  11. package/dist/durable-object/wal.d.ts +111 -0
  12. package/dist/durable-object/wal.d.ts.map +1 -0
  13. package/dist/durable-object/wal.js +200 -0
  14. package/dist/durable-object/wal.js.map +1 -0
  15. package/dist/index.d.ts +24 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +101 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/mcp/adapter.d.ts +231 -0
  20. package/dist/mcp/adapter.d.ts.map +1 -0
  21. package/dist/mcp/adapter.js +502 -0
  22. package/dist/mcp/adapter.js.map +1 -0
  23. package/dist/mcp/sandbox.d.ts +261 -0
  24. package/dist/mcp/sandbox.d.ts.map +1 -0
  25. package/dist/mcp/sandbox.js +983 -0
  26. package/dist/mcp/sandbox.js.map +1 -0
  27. package/dist/mcp/sdk-adapter.d.ts +413 -0
  28. package/dist/mcp/sdk-adapter.d.ts.map +1 -0
  29. package/dist/mcp/sdk-adapter.js +672 -0
  30. package/dist/mcp/sdk-adapter.js.map +1 -0
  31. package/dist/mcp/tools.d.ts +133 -0
  32. package/dist/mcp/tools.d.ts.map +1 -0
  33. package/dist/mcp/tools.js +1604 -0
  34. package/dist/mcp/tools.js.map +1 -0
  35. package/dist/ops/blame.d.ts +148 -0
  36. package/dist/ops/blame.d.ts.map +1 -0
  37. package/dist/ops/blame.js +754 -0
  38. package/dist/ops/blame.js.map +1 -0
  39. package/dist/ops/branch.d.ts +215 -0
  40. package/dist/ops/branch.d.ts.map +1 -0
  41. package/dist/ops/branch.js +608 -0
  42. package/dist/ops/branch.js.map +1 -0
  43. package/dist/ops/commit-traversal.d.ts +209 -0
  44. package/dist/ops/commit-traversal.d.ts.map +1 -0
  45. package/dist/ops/commit-traversal.js +755 -0
  46. package/dist/ops/commit-traversal.js.map +1 -0
  47. package/dist/ops/commit.d.ts +221 -0
  48. package/dist/ops/commit.d.ts.map +1 -0
  49. package/dist/ops/commit.js +606 -0
  50. package/dist/ops/commit.js.map +1 -0
  51. package/dist/ops/merge-base.d.ts +223 -0
  52. package/dist/ops/merge-base.d.ts.map +1 -0
  53. package/dist/ops/merge-base.js +581 -0
  54. package/dist/ops/merge-base.js.map +1 -0
  55. package/dist/ops/merge.d.ts +385 -0
  56. package/dist/ops/merge.d.ts.map +1 -0
  57. package/dist/ops/merge.js +1203 -0
  58. package/dist/ops/merge.js.map +1 -0
  59. package/dist/ops/tag.d.ts +182 -0
  60. package/dist/ops/tag.d.ts.map +1 -0
  61. package/dist/ops/tag.js +608 -0
  62. package/dist/ops/tag.js.map +1 -0
  63. package/dist/ops/tree-builder.d.ts +82 -0
  64. package/dist/ops/tree-builder.d.ts.map +1 -0
  65. package/dist/ops/tree-builder.js +246 -0
  66. package/dist/ops/tree-builder.js.map +1 -0
  67. package/dist/ops/tree-diff.d.ts +243 -0
  68. package/dist/ops/tree-diff.d.ts.map +1 -0
  69. package/dist/ops/tree-diff.js +657 -0
  70. package/dist/ops/tree-diff.js.map +1 -0
  71. package/dist/pack/delta.d.ts +68 -0
  72. package/dist/pack/delta.d.ts.map +1 -0
  73. package/dist/pack/delta.js +343 -0
  74. package/dist/pack/delta.js.map +1 -0
  75. package/dist/pack/format.d.ts +84 -0
  76. package/dist/pack/format.d.ts.map +1 -0
  77. package/dist/pack/format.js +261 -0
  78. package/dist/pack/format.js.map +1 -0
  79. package/dist/pack/full-generation.d.ts +327 -0
  80. package/dist/pack/full-generation.d.ts.map +1 -0
  81. package/dist/pack/full-generation.js +1159 -0
  82. package/dist/pack/full-generation.js.map +1 -0
  83. package/dist/pack/generation.d.ts +118 -0
  84. package/dist/pack/generation.d.ts.map +1 -0
  85. package/dist/pack/generation.js +459 -0
  86. package/dist/pack/generation.js.map +1 -0
  87. package/dist/pack/index.d.ts +181 -0
  88. package/dist/pack/index.d.ts.map +1 -0
  89. package/dist/pack/index.js +552 -0
  90. package/dist/pack/index.js.map +1 -0
  91. package/dist/refs/branch.d.ts +224 -0
  92. package/dist/refs/branch.d.ts.map +1 -0
  93. package/dist/refs/branch.js +170 -0
  94. package/dist/refs/branch.js.map +1 -0
  95. package/dist/refs/storage.d.ts +208 -0
  96. package/dist/refs/storage.d.ts.map +1 -0
  97. package/dist/refs/storage.js +421 -0
  98. package/dist/refs/storage.js.map +1 -0
  99. package/dist/refs/tag.d.ts +230 -0
  100. package/dist/refs/tag.d.ts.map +1 -0
  101. package/dist/refs/tag.js +188 -0
  102. package/dist/refs/tag.js.map +1 -0
  103. package/dist/storage/lru-cache.d.ts +188 -0
  104. package/dist/storage/lru-cache.d.ts.map +1 -0
  105. package/dist/storage/lru-cache.js +410 -0
  106. package/dist/storage/lru-cache.js.map +1 -0
  107. package/dist/storage/object-index.d.ts +140 -0
  108. package/dist/storage/object-index.d.ts.map +1 -0
  109. package/dist/storage/object-index.js +166 -0
  110. package/dist/storage/object-index.js.map +1 -0
  111. package/dist/storage/r2-pack.d.ts +394 -0
  112. package/dist/storage/r2-pack.d.ts.map +1 -0
  113. package/dist/storage/r2-pack.js +1062 -0
  114. package/dist/storage/r2-pack.js.map +1 -0
  115. package/dist/tiered/cdc-pipeline.d.ts +316 -0
  116. package/dist/tiered/cdc-pipeline.d.ts.map +1 -0
  117. package/dist/tiered/cdc-pipeline.js +771 -0
  118. package/dist/tiered/cdc-pipeline.js.map +1 -0
  119. package/dist/tiered/migration.d.ts +242 -0
  120. package/dist/tiered/migration.d.ts.map +1 -0
  121. package/dist/tiered/migration.js +592 -0
  122. package/dist/tiered/migration.js.map +1 -0
  123. package/dist/tiered/parquet-writer.d.ts +248 -0
  124. package/dist/tiered/parquet-writer.d.ts.map +1 -0
  125. package/dist/tiered/parquet-writer.js +555 -0
  126. package/dist/tiered/parquet-writer.js.map +1 -0
  127. package/dist/tiered/read-path.d.ts +141 -0
  128. package/dist/tiered/read-path.d.ts.map +1 -0
  129. package/dist/tiered/read-path.js +204 -0
  130. package/dist/tiered/read-path.js.map +1 -0
  131. package/dist/types/objects.d.ts +53 -0
  132. package/dist/types/objects.d.ts.map +1 -0
  133. package/dist/types/objects.js +291 -0
  134. package/dist/types/objects.js.map +1 -0
  135. package/dist/types/storage.d.ts +117 -0
  136. package/dist/types/storage.d.ts.map +1 -0
  137. package/dist/types/storage.js +8 -0
  138. package/dist/types/storage.js.map +1 -0
  139. package/dist/utils/hash.d.ts +31 -0
  140. package/dist/utils/hash.d.ts.map +1 -0
  141. package/dist/utils/hash.js +60 -0
  142. package/dist/utils/hash.js.map +1 -0
  143. package/dist/utils/sha1.d.ts +26 -0
  144. package/dist/utils/sha1.d.ts.map +1 -0
  145. package/dist/utils/sha1.js +127 -0
  146. package/dist/utils/sha1.js.map +1 -0
  147. package/dist/wire/capabilities.d.ts +236 -0
  148. package/dist/wire/capabilities.d.ts.map +1 -0
  149. package/dist/wire/capabilities.js +437 -0
  150. package/dist/wire/capabilities.js.map +1 -0
  151. package/dist/wire/pkt-line.d.ts +67 -0
  152. package/dist/wire/pkt-line.d.ts.map +1 -0
  153. package/dist/wire/pkt-line.js +145 -0
  154. package/dist/wire/pkt-line.js.map +1 -0
  155. package/dist/wire/receive-pack.d.ts +302 -0
  156. package/dist/wire/receive-pack.d.ts.map +1 -0
  157. package/dist/wire/receive-pack.js +885 -0
  158. package/dist/wire/receive-pack.js.map +1 -0
  159. package/dist/wire/smart-http.d.ts +321 -0
  160. package/dist/wire/smart-http.d.ts.map +1 -0
  161. package/dist/wire/smart-http.js +654 -0
  162. package/dist/wire/smart-http.js.map +1 -0
  163. package/dist/wire/upload-pack.d.ts +333 -0
  164. package/dist/wire/upload-pack.d.ts.map +1 -0
  165. package/dist/wire/upload-pack.js +850 -0
  166. package/dist/wire/upload-pack.js.map +1 -0
  167. package/package.json +61 -0
@@ -0,0 +1,1604 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Git Tool Definitions
3
+ *
4
+ * This module provides tool definitions for git operations that can be
5
+ * exposed via the Model Context Protocol for AI assistants.
6
+ */
7
+ import { walkCommits } from '../ops/commit-traversal';
8
+ import { diffTrees, DiffStatus } from '../ops/tree-diff';
9
+ import { listBranches, createBranch, deleteBranch, getCurrentBranch } from '../ops/branch';
10
+ import { createCommit } from '../ops/commit';
11
+ /** Global repository context - set by the application before invoking tools */
12
+ let globalRepositoryContext = null;
13
+ /**
14
+ * Set the global repository context for MCP tools
15
+ */
16
+ export function setRepositoryContext(ctx) {
17
+ globalRepositoryContext = ctx;
18
+ }
19
+ /**
20
+ * Get the global repository context
21
+ */
22
+ export function getRepositoryContext() {
23
+ return globalRepositoryContext;
24
+ }
25
+ /**
26
+ * Validate a path parameter to prevent command injection
27
+ * @param path - The path to validate
28
+ * @returns The validated path (defaults to '.' if undefined)
29
+ * @throws Error if path contains forbidden characters
30
+ */
31
+ function validatePath(path) {
32
+ if (!path)
33
+ return '.';
34
+ // Reject path traversal attempts
35
+ if (path.includes('..') || path.startsWith('/') || /[<>|&;$`]/.test(path)) {
36
+ throw new Error('Invalid path: contains forbidden characters');
37
+ }
38
+ return path;
39
+ }
40
+ /**
41
+ * Validate a branch or ref name according to git rules
42
+ * @param name - The branch/ref name to validate
43
+ * @returns The validated name
44
+ * @throws Error if name contains invalid characters
45
+ */
46
+ function validateBranchName(name) {
47
+ // Git branch name rules
48
+ if (!/^[a-zA-Z0-9._\/-]+$/.test(name) || name.includes('..')) {
49
+ throw new Error('Invalid branch name');
50
+ }
51
+ return name;
52
+ }
53
+ /**
54
+ * Validate a commit reference (hash, branch, tag, HEAD, etc.)
55
+ * @param ref - The commit reference to validate
56
+ * @returns The validated reference
57
+ * @throws Error if reference contains invalid characters
58
+ */
59
+ function validateCommitRef(ref) {
60
+ // Allow hex hashes, branch names, tags, HEAD, HEAD~n, HEAD^n, etc.
61
+ if (!/^[a-zA-Z0-9._\/-~^]+$/.test(ref) || ref.includes('..')) {
62
+ throw new Error('Invalid commit reference');
63
+ }
64
+ return ref;
65
+ }
66
+ /**
67
+ * Validate a URL for git clone operations
68
+ * @param url - The URL to validate
69
+ * @returns The validated URL
70
+ * @throws Error if URL contains shell injection characters
71
+ */
72
+ function validateUrl(url) {
73
+ // Reject shell injection characters in URLs
74
+ if (/[<>|&;$`]/.test(url)) {
75
+ throw new Error('Invalid URL: contains forbidden characters');
76
+ }
77
+ return url;
78
+ }
79
+ /**
80
+ * Validate a remote name
81
+ * @param name - The remote name to validate
82
+ * @returns The validated name
83
+ * @throws Error if name contains invalid characters
84
+ */
85
+ function validateRemoteName(name) {
86
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
87
+ throw new Error('Invalid remote name');
88
+ }
89
+ return name;
90
+ }
91
+ /**
92
+ * Convert DiffStatus to human-readable text
93
+ */
94
+ function getStatusText(status) {
95
+ switch (status) {
96
+ case DiffStatus.ADDED:
97
+ return 'new file';
98
+ case DiffStatus.DELETED:
99
+ return 'deleted';
100
+ case DiffStatus.MODIFIED:
101
+ return 'modified';
102
+ case DiffStatus.RENAMED:
103
+ return 'renamed';
104
+ case DiffStatus.COPIED:
105
+ return 'copied';
106
+ case DiffStatus.TYPE_CHANGED:
107
+ return 'typechange';
108
+ case DiffStatus.UNMERGED:
109
+ return 'unmerged';
110
+ default:
111
+ return 'unknown';
112
+ }
113
+ }
114
+ /**
115
+ * Format a commit for log output
116
+ */
117
+ function formatCommit(sha, commit, oneline) {
118
+ if (oneline) {
119
+ const subject = commit.message.split('\n')[0];
120
+ return `${sha.slice(0, 7)} ${subject}`;
121
+ }
122
+ const lines = [];
123
+ lines.push(`commit ${sha}`);
124
+ lines.push(`Author: ${commit.author.name} <${commit.author.email}>`);
125
+ const date = new Date(commit.author.timestamp * 1000);
126
+ lines.push(`Date: ${date.toUTCString()}`);
127
+ lines.push('');
128
+ // Indent the commit message
129
+ const messageLines = commit.message.split('\n');
130
+ for (const line of messageLines) {
131
+ lines.push(` ${line}`);
132
+ }
133
+ lines.push('');
134
+ return lines.join('\n');
135
+ }
136
+ /**
137
+ * Internal registry for custom-registered tools
138
+ */
139
+ const toolRegistry = new Map();
140
+ /**
141
+ * Registry of available git tools
142
+ */
143
+ export const gitTools = [
144
+ // git_status tool
145
+ {
146
+ name: 'git_status',
147
+ description: 'Get the current status of a git repository, showing staged, unstaged, and untracked files',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ path: {
152
+ type: 'string',
153
+ description: 'Path to the git repository',
154
+ },
155
+ short: {
156
+ type: 'boolean',
157
+ description: 'Show short-format output',
158
+ },
159
+ },
160
+ },
161
+ handler: async (params) => {
162
+ const { short } = params;
163
+ const ctx = globalRepositoryContext;
164
+ // If no repository context, return mock response for backward compatibility
165
+ if (!ctx) {
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: 'No repository context available. Set repository context with setRepositoryContext().',
171
+ },
172
+ ],
173
+ isError: true,
174
+ };
175
+ }
176
+ try {
177
+ // Get current branch
178
+ const currentBranch = await getCurrentBranch(ctx.refStore);
179
+ // Get HEAD commit SHA
180
+ const headRef = await ctx.refStore.getSymbolicRef('HEAD');
181
+ let headSha = null;
182
+ if (headRef) {
183
+ headSha = await ctx.refStore.getRef(headRef);
184
+ }
185
+ else {
186
+ headSha = await ctx.refStore.getHead();
187
+ }
188
+ // Build status output
189
+ const lines = [];
190
+ if (!short) {
191
+ if (currentBranch) {
192
+ lines.push(`On branch ${currentBranch}`);
193
+ }
194
+ else {
195
+ lines.push(`HEAD detached at ${headSha?.slice(0, 7) || 'unknown'}`);
196
+ }
197
+ lines.push('');
198
+ }
199
+ // Get staged changes (index vs HEAD)
200
+ let stagedChanges = null;
201
+ if (headSha && ctx.index) {
202
+ const headCommit = await ctx.objectStore.getCommit(headSha);
203
+ if (headCommit) {
204
+ // Get index entries for future tree building
205
+ // Note: Full implementation would build a tree from these entries
206
+ void ctx.index.getEntries(); // Acknowledge index exists but tree building not yet implemented
207
+ const diffStore = {
208
+ getTree: (sha) => ctx.objectStore.getTree(sha),
209
+ getBlob: (sha) => ctx.objectStore.getBlob(sha),
210
+ exists: (sha) => ctx.objectStore.hasObject(sha)
211
+ };
212
+ stagedChanges = await diffTrees(diffStore, headCommit.tree, null, // TODO: Build tree from index entries for proper staging area comparison
213
+ { recursive: true });
214
+ }
215
+ }
216
+ // Format staged changes
217
+ if (stagedChanges && stagedChanges.entries.length > 0) {
218
+ if (!short) {
219
+ lines.push('Changes to be committed:');
220
+ lines.push(' (use "git restore --staged <file>..." to unstage)');
221
+ lines.push('');
222
+ }
223
+ for (const entry of stagedChanges.entries) {
224
+ const statusChar = entry.status;
225
+ if (short) {
226
+ lines.push(`${statusChar} ${entry.path}`);
227
+ }
228
+ else {
229
+ const statusText = getStatusText(entry.status);
230
+ lines.push(` ${statusText}: ${entry.path}`);
231
+ }
232
+ }
233
+ if (!short)
234
+ lines.push('');
235
+ }
236
+ // If no changes
237
+ if (!stagedChanges || stagedChanges.entries.length === 0) {
238
+ if (!short) {
239
+ lines.push('nothing to commit, working tree clean');
240
+ }
241
+ }
242
+ return {
243
+ content: [
244
+ {
245
+ type: 'text',
246
+ text: lines.join('\n'),
247
+ },
248
+ ],
249
+ };
250
+ }
251
+ catch (error) {
252
+ return {
253
+ content: [
254
+ {
255
+ type: 'text',
256
+ text: `Error getting status: ${error instanceof Error ? error.message : String(error)}`,
257
+ },
258
+ ],
259
+ isError: true,
260
+ };
261
+ }
262
+ },
263
+ },
264
+ // git_log tool
265
+ {
266
+ name: 'git_log',
267
+ description: 'Show the commit log history for a git repository',
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {
271
+ path: {
272
+ type: 'string',
273
+ description: 'Path to the git repository',
274
+ },
275
+ maxCount: {
276
+ type: 'number',
277
+ description: 'Maximum number of commits to show',
278
+ minimum: 1,
279
+ },
280
+ oneline: {
281
+ type: 'boolean',
282
+ description: 'Show each commit on a single line',
283
+ },
284
+ ref: {
285
+ type: 'string',
286
+ description: 'Branch, tag, or commit reference to show log for',
287
+ },
288
+ },
289
+ },
290
+ handler: async (params) => {
291
+ const { maxCount, oneline, ref } = params;
292
+ const ctx = globalRepositoryContext;
293
+ // If no repository context, return error
294
+ if (!ctx) {
295
+ return {
296
+ content: [
297
+ {
298
+ type: 'text',
299
+ text: 'No repository context available. Set repository context with setRepositoryContext().',
300
+ },
301
+ ],
302
+ isError: true,
303
+ };
304
+ }
305
+ try {
306
+ // Resolve starting commit
307
+ let startSha = null;
308
+ if (ref) {
309
+ // Validate and resolve ref
310
+ const validatedRef = validateCommitRef(ref);
311
+ // Try as branch first
312
+ startSha = await ctx.refStore.getRef(`refs/heads/${validatedRef}`);
313
+ // Try as direct SHA if not found
314
+ if (!startSha && /^[a-f0-9]{40}$/i.test(validatedRef)) {
315
+ startSha = validatedRef;
316
+ }
317
+ // Try as tag
318
+ if (!startSha) {
319
+ startSha = await ctx.refStore.getRef(`refs/tags/${validatedRef}`);
320
+ }
321
+ }
322
+ else {
323
+ // Use HEAD
324
+ const headRef = await ctx.refStore.getSymbolicRef('HEAD');
325
+ if (headRef) {
326
+ startSha = await ctx.refStore.getRef(headRef);
327
+ }
328
+ else {
329
+ startSha = await ctx.refStore.getHead();
330
+ }
331
+ }
332
+ if (!startSha) {
333
+ return {
334
+ content: [
335
+ {
336
+ type: 'text',
337
+ text: ref ? `fatal: bad revision '${ref}'` : 'fatal: HEAD not found',
338
+ },
339
+ ],
340
+ isError: true,
341
+ };
342
+ }
343
+ // Create commit provider adapter
344
+ const commitProvider = {
345
+ getCommit: async (sha) => ctx.objectStore.getCommit(sha)
346
+ };
347
+ // Walk commits
348
+ const traversalOptions = {
349
+ maxCount: maxCount,
350
+ sort: 'date'
351
+ };
352
+ const commits = [];
353
+ for await (const traversalCommit of walkCommits(commitProvider, startSha, traversalOptions)) {
354
+ commits.push(formatCommit(traversalCommit.sha, traversalCommit.commit, oneline || false));
355
+ }
356
+ const output = commits.join(oneline ? '\n' : '');
357
+ return {
358
+ content: [
359
+ {
360
+ type: 'text',
361
+ text: output || 'No commits found',
362
+ },
363
+ ],
364
+ };
365
+ }
366
+ catch (error) {
367
+ return {
368
+ content: [
369
+ {
370
+ type: 'text',
371
+ text: `Error getting log: ${error instanceof Error ? error.message : String(error)}`,
372
+ },
373
+ ],
374
+ isError: true,
375
+ };
376
+ }
377
+ },
378
+ },
379
+ // git_diff tool
380
+ {
381
+ name: 'git_diff',
382
+ description: 'Show differences between commits, commit and working tree',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ path: {
387
+ type: 'string',
388
+ description: 'Path to the git repository',
389
+ },
390
+ staged: {
391
+ type: 'boolean',
392
+ description: 'Show staged changes (--cached)',
393
+ },
394
+ commit1: {
395
+ type: 'string',
396
+ description: 'First commit to compare',
397
+ },
398
+ commit2: {
399
+ type: 'string',
400
+ description: 'Second commit to compare',
401
+ },
402
+ },
403
+ },
404
+ handler: async (params) => {
405
+ const { staged, commit1, commit2 } = params;
406
+ const ctx = globalRepositoryContext;
407
+ // If no repository context, return error
408
+ if (!ctx) {
409
+ return {
410
+ content: [
411
+ {
412
+ type: 'text',
413
+ text: 'No repository context available. Set repository context with setRepositoryContext().',
414
+ },
415
+ ],
416
+ isError: true,
417
+ };
418
+ }
419
+ try {
420
+ // Create diff store adapter
421
+ const diffStore = {
422
+ getTree: (sha) => ctx.objectStore.getTree(sha),
423
+ getBlob: (sha) => ctx.objectStore.getBlob(sha),
424
+ exists: (sha) => ctx.objectStore.hasObject(sha)
425
+ };
426
+ let oldTreeSha = null;
427
+ let newTreeSha = null;
428
+ // Resolve commits to tree SHAs
429
+ const resolveCommitToTree = async (commitRef) => {
430
+ // Validate ref
431
+ const validatedRef = validateCommitRef(commitRef);
432
+ // Try as direct SHA
433
+ if (/^[a-f0-9]{40}$/i.test(validatedRef)) {
434
+ const commit = await ctx.objectStore.getCommit(validatedRef);
435
+ return commit?.tree || null;
436
+ }
437
+ // Try as branch
438
+ let sha = await ctx.refStore.getRef(`refs/heads/${validatedRef}`);
439
+ if (!sha) {
440
+ sha = await ctx.refStore.getRef(`refs/tags/${validatedRef}`);
441
+ }
442
+ if (sha) {
443
+ const commit = await ctx.objectStore.getCommit(sha);
444
+ return commit?.tree || null;
445
+ }
446
+ return null;
447
+ };
448
+ if (commit1 && commit2) {
449
+ // Compare two commits
450
+ oldTreeSha = await resolveCommitToTree(commit1);
451
+ newTreeSha = await resolveCommitToTree(commit2);
452
+ }
453
+ else if (commit1) {
454
+ // Compare commit to HEAD
455
+ oldTreeSha = await resolveCommitToTree(commit1);
456
+ // Get HEAD tree
457
+ const headRef = await ctx.refStore.getSymbolicRef('HEAD');
458
+ let headSha = null;
459
+ if (headRef) {
460
+ headSha = await ctx.refStore.getRef(headRef);
461
+ }
462
+ else {
463
+ headSha = await ctx.refStore.getHead();
464
+ }
465
+ if (headSha) {
466
+ const headCommit = await ctx.objectStore.getCommit(headSha);
467
+ newTreeSha = headCommit?.tree || null;
468
+ }
469
+ }
470
+ else if (staged) {
471
+ // Compare HEAD to index (staged changes)
472
+ const headRef = await ctx.refStore.getSymbolicRef('HEAD');
473
+ let headSha = null;
474
+ if (headRef) {
475
+ headSha = await ctx.refStore.getRef(headRef);
476
+ }
477
+ else {
478
+ headSha = await ctx.refStore.getHead();
479
+ }
480
+ if (headSha) {
481
+ const headCommit = await ctx.objectStore.getCommit(headSha);
482
+ oldTreeSha = headCommit?.tree || null;
483
+ }
484
+ // For staged diff, we would compare against index
485
+ // newTreeSha would be built from index entries
486
+ newTreeSha = null; // Index comparison not fully implemented
487
+ }
488
+ else {
489
+ // Default: compare working tree to index (unstaged changes)
490
+ // This requires working directory support
491
+ return {
492
+ content: [
493
+ {
494
+ type: 'text',
495
+ text: 'Working tree diff requires workdir context (not yet implemented)',
496
+ },
497
+ ],
498
+ };
499
+ }
500
+ if (oldTreeSha === null && newTreeSha === null) {
501
+ return {
502
+ content: [
503
+ {
504
+ type: 'text',
505
+ text: 'No changes to display',
506
+ },
507
+ ],
508
+ };
509
+ }
510
+ // Perform the diff
511
+ const diffResult = await diffTrees(diffStore, oldTreeSha, newTreeSha, {
512
+ recursive: true,
513
+ detectRenames: true
514
+ });
515
+ // Format diff output
516
+ const lines = [];
517
+ for (const entry of diffResult.entries) {
518
+ lines.push(`diff --git a/${entry.oldPath || entry.path} b/${entry.path}`);
519
+ if (entry.status === DiffStatus.ADDED) {
520
+ lines.push('new file mode ' + entry.newMode);
521
+ }
522
+ else if (entry.status === DiffStatus.DELETED) {
523
+ lines.push('deleted file mode ' + entry.oldMode);
524
+ }
525
+ else if (entry.status === DiffStatus.RENAMED) {
526
+ lines.push(`rename from ${entry.oldPath}`);
527
+ lines.push(`rename to ${entry.path}`);
528
+ if (entry.similarity !== undefined) {
529
+ lines.push(`similarity index ${entry.similarity}%`);
530
+ }
531
+ }
532
+ lines.push(`index ${entry.oldSha?.slice(0, 7) || '0000000'}..${entry.newSha?.slice(0, 7) || '0000000'}`);
533
+ lines.push(`--- ${entry.status === DiffStatus.ADDED ? '/dev/null' : 'a/' + (entry.oldPath || entry.path)}`);
534
+ lines.push(`+++ ${entry.status === DiffStatus.DELETED ? '/dev/null' : 'b/' + entry.path}`);
535
+ lines.push(''); // Placeholder for actual content diff
536
+ }
537
+ // Add stats summary
538
+ lines.push('');
539
+ lines.push(`${diffResult.entries.length} file(s) changed`);
540
+ return {
541
+ content: [
542
+ {
543
+ type: 'text',
544
+ text: lines.join('\n') || 'No changes',
545
+ },
546
+ ],
547
+ };
548
+ }
549
+ catch (error) {
550
+ return {
551
+ content: [
552
+ {
553
+ type: 'text',
554
+ text: `Error getting diff: ${error instanceof Error ? error.message : String(error)}`,
555
+ },
556
+ ],
557
+ isError: true,
558
+ };
559
+ }
560
+ },
561
+ },
562
+ // git_commit tool
563
+ {
564
+ name: 'git_commit',
565
+ description: 'Create a new commit with the staged changes in the repository',
566
+ inputSchema: {
567
+ type: 'object',
568
+ properties: {
569
+ path: {
570
+ type: 'string',
571
+ description: 'Path to the git repository',
572
+ },
573
+ message: {
574
+ type: 'string',
575
+ description: 'Commit message',
576
+ },
577
+ author: {
578
+ type: 'string',
579
+ description: 'Author name for the commit',
580
+ },
581
+ email: {
582
+ type: 'string',
583
+ description: 'Author email for the commit',
584
+ },
585
+ amend: {
586
+ type: 'boolean',
587
+ description: 'Amend the previous commit',
588
+ },
589
+ },
590
+ required: ['message'],
591
+ },
592
+ handler: async (params) => {
593
+ const { message, author, email, amend } = params;
594
+ const ctx = globalRepositoryContext;
595
+ // If no repository context, return error
596
+ if (!ctx) {
597
+ return {
598
+ content: [
599
+ {
600
+ type: 'text',
601
+ text: 'No repository context available. Set repository context with setRepositoryContext().',
602
+ },
603
+ ],
604
+ isError: true,
605
+ };
606
+ }
607
+ // Sanitize message - reject shell injection characters (for backward compat)
608
+ if (/[`$]/.test(message)) {
609
+ throw new Error('Invalid commit message: contains forbidden characters');
610
+ }
611
+ // Validate author and email if provided
612
+ if (author && email) {
613
+ if (/[<>"`$\\]/.test(author) || /[<>"`$\\]/.test(email)) {
614
+ throw new Error('Invalid author/email: contains forbidden characters');
615
+ }
616
+ }
617
+ try {
618
+ // Get current HEAD
619
+ const headRef = await ctx.refStore.getSymbolicRef('HEAD');
620
+ let parentSha = null;
621
+ if (headRef) {
622
+ parentSha = await ctx.refStore.getRef(headRef);
623
+ }
624
+ else {
625
+ parentSha = await ctx.refStore.getHead();
626
+ }
627
+ // For a real commit, we need:
628
+ // 1. A tree SHA from the index
629
+ // 2. Parent commit(s)
630
+ // 3. Author/committer info
631
+ // If we don't have an index, we can't create a real commit
632
+ if (!ctx.index) {
633
+ return {
634
+ content: [
635
+ {
636
+ type: 'text',
637
+ text: 'Cannot create commit: no index/staging area available',
638
+ },
639
+ ],
640
+ isError: true,
641
+ };
642
+ }
643
+ // Get index entries and build tree
644
+ // For now, we need a tree SHA - in a full implementation we'd build it from index
645
+ // This is a simplified version that requires the tree to already exist
646
+ const now = Math.floor(Date.now() / 1000);
647
+ const timezone = '+0000'; // UTC for simplicity
648
+ const commitAuthor = {
649
+ name: author || 'Unknown',
650
+ email: email || 'unknown@example.com',
651
+ timestamp: now,
652
+ timezone
653
+ };
654
+ // For amend, get the parent's tree (simplified)
655
+ let treeSha = null;
656
+ const parents = [];
657
+ if (amend && parentSha) {
658
+ // Get parent commit for amend
659
+ const parentCommit = await ctx.objectStore.getCommit(parentSha);
660
+ if (parentCommit) {
661
+ treeSha = parentCommit.tree;
662
+ parents.push(...parentCommit.parents);
663
+ }
664
+ }
665
+ else if (parentSha) {
666
+ // Regular commit - parent is current HEAD
667
+ const parentCommit = await ctx.objectStore.getCommit(parentSha);
668
+ if (parentCommit) {
669
+ treeSha = parentCommit.tree; // Use parent's tree for now (no changes)
670
+ }
671
+ parents.push(parentSha);
672
+ }
673
+ if (!treeSha) {
674
+ return {
675
+ content: [
676
+ {
677
+ type: 'text',
678
+ text: 'Cannot create commit: unable to determine tree SHA',
679
+ },
680
+ ],
681
+ isError: true,
682
+ };
683
+ }
684
+ // Create the commit using gitdo's commit creation
685
+ const commitOptions = {
686
+ message,
687
+ tree: treeSha,
688
+ parents,
689
+ author: commitAuthor,
690
+ committer: commitAuthor,
691
+ allowEmpty: true
692
+ };
693
+ // Create object store adapter for createCommit
694
+ const commitStore = {
695
+ getObject: ctx.objectStore.getObject,
696
+ storeObject: ctx.objectStore.storeObject,
697
+ hasObject: ctx.objectStore.hasObject
698
+ };
699
+ const result = await createCommit(commitStore, commitOptions);
700
+ // Update the ref to point to the new commit
701
+ if (headRef) {
702
+ await ctx.refStore.setRef(headRef, result.sha);
703
+ }
704
+ return {
705
+ content: [
706
+ {
707
+ type: 'text',
708
+ text: `[${headRef ? headRef.replace('refs/heads/', '') : 'detached HEAD'} ${result.sha.slice(0, 7)}] ${message.split('\n')[0]}`,
709
+ },
710
+ ],
711
+ };
712
+ }
713
+ catch (error) {
714
+ return {
715
+ content: [
716
+ {
717
+ type: 'text',
718
+ text: `Error creating commit: ${error instanceof Error ? error.message : String(error)}`,
719
+ },
720
+ ],
721
+ isError: true,
722
+ };
723
+ }
724
+ },
725
+ },
726
+ // git_branch tool
727
+ {
728
+ name: 'git_branch',
729
+ description: 'List, create, or delete branches in the repository',
730
+ inputSchema: {
731
+ type: 'object',
732
+ properties: {
733
+ path: {
734
+ type: 'string',
735
+ description: 'Path to the git repository',
736
+ },
737
+ list: {
738
+ type: 'boolean',
739
+ description: 'List branches',
740
+ },
741
+ name: {
742
+ type: 'string',
743
+ description: 'Name of the branch to create or delete',
744
+ },
745
+ delete: {
746
+ type: 'boolean',
747
+ description: 'Delete the specified branch',
748
+ },
749
+ all: {
750
+ type: 'boolean',
751
+ description: 'List all branches including remote branches',
752
+ },
753
+ },
754
+ },
755
+ handler: async (params) => {
756
+ const { list, name, delete: del, all } = params;
757
+ const ctx = globalRepositoryContext;
758
+ // If no repository context, return error
759
+ if (!ctx) {
760
+ return {
761
+ content: [
762
+ {
763
+ type: 'text',
764
+ text: 'No repository context available. Set repository context with setRepositoryContext().',
765
+ },
766
+ ],
767
+ isError: true,
768
+ };
769
+ }
770
+ try {
771
+ // List branches
772
+ if (list || (!name && !del)) {
773
+ const branches = await listBranches(ctx.refStore, {
774
+ all: all || false,
775
+ remote: false
776
+ });
777
+ if (branches.length === 0) {
778
+ return {
779
+ content: [
780
+ {
781
+ type: 'text',
782
+ text: 'No branches found',
783
+ },
784
+ ],
785
+ };
786
+ }
787
+ const lines = [];
788
+ for (const branch of branches) {
789
+ const prefix = branch.current ? '* ' : ' ';
790
+ lines.push(`${prefix}${branch.name}`);
791
+ }
792
+ return {
793
+ content: [
794
+ {
795
+ type: 'text',
796
+ text: lines.join('\n'),
797
+ },
798
+ ],
799
+ };
800
+ }
801
+ // Delete branch
802
+ if (del && name) {
803
+ const validatedName = validateBranchName(name);
804
+ const result = await deleteBranch(ctx.refStore, { name: validatedName });
805
+ return {
806
+ content: [
807
+ {
808
+ type: 'text',
809
+ text: `Deleted branch ${validatedName} (was ${result.sha.slice(0, 7)}).`,
810
+ },
811
+ ],
812
+ };
813
+ }
814
+ // Create branch
815
+ if (name) {
816
+ const validatedName = validateBranchName(name);
817
+ const result = await createBranch(ctx.refStore, { name: validatedName });
818
+ return {
819
+ content: [
820
+ {
821
+ type: 'text',
822
+ text: result.created
823
+ ? `Created branch '${validatedName}' at ${result.sha.slice(0, 7)}`
824
+ : `Branch '${validatedName}' already exists at ${result.sha.slice(0, 7)}`,
825
+ },
826
+ ],
827
+ };
828
+ }
829
+ return {
830
+ content: [
831
+ {
832
+ type: 'text',
833
+ text: 'No branch operation specified',
834
+ },
835
+ ],
836
+ };
837
+ }
838
+ catch (error) {
839
+ return {
840
+ content: [
841
+ {
842
+ type: 'text',
843
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
844
+ },
845
+ ],
846
+ isError: true,
847
+ };
848
+ }
849
+ },
850
+ },
851
+ // git_checkout tool
852
+ {
853
+ name: 'git_checkout',
854
+ description: 'Switch branches or restore working tree files using git checkout',
855
+ inputSchema: {
856
+ type: 'object',
857
+ properties: {
858
+ path: {
859
+ type: 'string',
860
+ description: 'Path to the git repository',
861
+ },
862
+ ref: {
863
+ type: 'string',
864
+ description: 'Branch, tag, or commit to checkout',
865
+ },
866
+ createBranch: {
867
+ type: 'boolean',
868
+ description: 'Create a new branch with the given ref name',
869
+ },
870
+ },
871
+ required: ['ref'],
872
+ },
873
+ handler: async (params) => {
874
+ const { path, ref, createBranch } = params;
875
+ const validatedPath = validatePath(path);
876
+ const validatedRef = validateBranchName(ref);
877
+ const args = ['checkout'];
878
+ if (createBranch)
879
+ args.push('-b');
880
+ args.push(validatedRef);
881
+ return {
882
+ content: [
883
+ {
884
+ type: 'text',
885
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
886
+ },
887
+ ],
888
+ };
889
+ },
890
+ },
891
+ // git_push tool
892
+ {
893
+ name: 'git_push',
894
+ description: 'Upload local commits to a remote repository using git push',
895
+ inputSchema: {
896
+ type: 'object',
897
+ properties: {
898
+ path: {
899
+ type: 'string',
900
+ description: 'Path to the git repository',
901
+ },
902
+ remote: {
903
+ type: 'string',
904
+ description: 'Name of the remote (e.g., origin)',
905
+ },
906
+ branch: {
907
+ type: 'string',
908
+ description: 'Branch to push',
909
+ },
910
+ force: {
911
+ type: 'boolean',
912
+ description: 'Force push (use with caution)',
913
+ },
914
+ setUpstream: {
915
+ type: 'boolean',
916
+ description: 'Set upstream for the current branch',
917
+ },
918
+ },
919
+ },
920
+ handler: async (params) => {
921
+ const { path, remote, branch, force, setUpstream } = params;
922
+ const validatedPath = validatePath(path);
923
+ const args = ['push'];
924
+ if (force)
925
+ args.push('--force');
926
+ if (setUpstream)
927
+ args.push('-u');
928
+ if (remote)
929
+ args.push(validateRemoteName(remote));
930
+ if (branch)
931
+ args.push(validateBranchName(branch));
932
+ return {
933
+ content: [
934
+ {
935
+ type: 'text',
936
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
937
+ },
938
+ ],
939
+ };
940
+ },
941
+ },
942
+ // git_pull tool
943
+ {
944
+ name: 'git_pull',
945
+ description: 'Fetch and integrate changes from a remote repository using git pull',
946
+ inputSchema: {
947
+ type: 'object',
948
+ properties: {
949
+ path: {
950
+ type: 'string',
951
+ description: 'Path to the git repository',
952
+ },
953
+ remote: {
954
+ type: 'string',
955
+ description: 'Name of the remote (e.g., origin)',
956
+ },
957
+ branch: {
958
+ type: 'string',
959
+ description: 'Branch to pull',
960
+ },
961
+ rebase: {
962
+ type: 'boolean',
963
+ description: 'Rebase instead of merge',
964
+ },
965
+ },
966
+ },
967
+ handler: async (params) => {
968
+ const { path, remote, branch, rebase } = params;
969
+ const validatedPath = validatePath(path);
970
+ const args = ['pull'];
971
+ if (rebase)
972
+ args.push('--rebase');
973
+ if (remote)
974
+ args.push(validateRemoteName(remote));
975
+ if (branch)
976
+ args.push(validateBranchName(branch));
977
+ return {
978
+ content: [
979
+ {
980
+ type: 'text',
981
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
982
+ },
983
+ ],
984
+ };
985
+ },
986
+ },
987
+ // git_clone tool
988
+ {
989
+ name: 'git_clone',
990
+ description: 'Copy a repository from a remote URL to a local directory using git clone',
991
+ inputSchema: {
992
+ type: 'object',
993
+ properties: {
994
+ url: {
995
+ type: 'string',
996
+ description: 'URL of the repository to clone',
997
+ },
998
+ destination: {
999
+ type: 'string',
1000
+ description: 'Local path to clone into',
1001
+ },
1002
+ depth: {
1003
+ type: 'number',
1004
+ description: 'Create a shallow clone with specified depth',
1005
+ },
1006
+ branch: {
1007
+ type: 'string',
1008
+ description: 'Branch to clone',
1009
+ },
1010
+ bare: {
1011
+ type: 'boolean',
1012
+ description: 'Create a bare repository',
1013
+ },
1014
+ },
1015
+ required: ['url'],
1016
+ },
1017
+ handler: async (params) => {
1018
+ const { url, destination, depth, branch, bare } = params;
1019
+ const validatedUrl = validateUrl(url);
1020
+ const args = ['clone'];
1021
+ if (depth)
1022
+ args.push(`--depth=${depth}`);
1023
+ if (branch)
1024
+ args.push(`--branch=${validateBranchName(branch)}`);
1025
+ if (bare)
1026
+ args.push('--bare');
1027
+ args.push(validatedUrl);
1028
+ if (destination)
1029
+ args.push(validatePath(destination));
1030
+ return {
1031
+ content: [
1032
+ {
1033
+ type: 'text',
1034
+ text: `Executed: git ${args.join(' ')}`,
1035
+ },
1036
+ ],
1037
+ };
1038
+ },
1039
+ },
1040
+ // git_init tool
1041
+ {
1042
+ name: 'git_init',
1043
+ description: 'Create an empty git repository or reinitialize an existing one',
1044
+ inputSchema: {
1045
+ type: 'object',
1046
+ properties: {
1047
+ path: {
1048
+ type: 'string',
1049
+ description: 'Path where the repository should be initialized',
1050
+ },
1051
+ bare: {
1052
+ type: 'boolean',
1053
+ description: 'Create a bare repository',
1054
+ },
1055
+ initialBranch: {
1056
+ type: 'string',
1057
+ description: 'Name for the initial branch',
1058
+ },
1059
+ },
1060
+ required: ['path'],
1061
+ },
1062
+ handler: async (params) => {
1063
+ const { path, bare, initialBranch } = params;
1064
+ const validatedPath = validatePath(path);
1065
+ const args = ['init'];
1066
+ if (bare)
1067
+ args.push('--bare');
1068
+ if (initialBranch)
1069
+ args.push(`--initial-branch=${validateBranchName(initialBranch)}`);
1070
+ args.push(validatedPath);
1071
+ return {
1072
+ content: [
1073
+ {
1074
+ type: 'text',
1075
+ text: `Executed: git ${args.join(' ')}`,
1076
+ },
1077
+ ],
1078
+ };
1079
+ },
1080
+ },
1081
+ // git_add tool
1082
+ {
1083
+ name: 'git_add',
1084
+ description: 'Add file contents to the staging area for the next commit',
1085
+ inputSchema: {
1086
+ type: 'object',
1087
+ properties: {
1088
+ path: {
1089
+ type: 'string',
1090
+ description: 'Path to the git repository',
1091
+ },
1092
+ files: {
1093
+ type: 'array',
1094
+ items: { type: 'string' },
1095
+ description: 'List of files to add',
1096
+ },
1097
+ all: {
1098
+ type: 'boolean',
1099
+ description: 'Add all changes in the working tree',
1100
+ },
1101
+ force: {
1102
+ type: 'boolean',
1103
+ description: 'Allow adding otherwise ignored files',
1104
+ },
1105
+ },
1106
+ },
1107
+ handler: async (params) => {
1108
+ const { path, files, all, force } = params;
1109
+ const validatedPath = validatePath(path);
1110
+ const args = ['add'];
1111
+ if (all)
1112
+ args.push('--all');
1113
+ if (force)
1114
+ args.push('--force');
1115
+ if (files) {
1116
+ // Validate each file path
1117
+ const validatedFiles = files.map((f) => {
1118
+ if (/[<>|&;$`]/.test(f)) {
1119
+ throw new Error('Invalid file path: contains forbidden characters');
1120
+ }
1121
+ return f;
1122
+ });
1123
+ args.push(...validatedFiles);
1124
+ }
1125
+ return {
1126
+ content: [
1127
+ {
1128
+ type: 'text',
1129
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1130
+ },
1131
+ ],
1132
+ };
1133
+ },
1134
+ },
1135
+ // git_reset tool
1136
+ {
1137
+ name: 'git_reset',
1138
+ description: 'Reset current HEAD to a specified state',
1139
+ inputSchema: {
1140
+ type: 'object',
1141
+ properties: {
1142
+ path: {
1143
+ type: 'string',
1144
+ description: 'Path to the git repository',
1145
+ },
1146
+ mode: {
1147
+ type: 'string',
1148
+ enum: ['soft', 'mixed', 'hard'],
1149
+ description: 'Reset mode: soft, mixed, or hard',
1150
+ },
1151
+ commit: {
1152
+ type: 'string',
1153
+ description: 'Commit to reset to',
1154
+ },
1155
+ },
1156
+ },
1157
+ handler: async (params) => {
1158
+ const { path, mode, commit } = params;
1159
+ const validatedPath = validatePath(path);
1160
+ const args = ['reset'];
1161
+ if (mode)
1162
+ args.push(`--${mode}`);
1163
+ if (commit)
1164
+ args.push(validateCommitRef(commit));
1165
+ return {
1166
+ content: [
1167
+ {
1168
+ type: 'text',
1169
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1170
+ },
1171
+ ],
1172
+ };
1173
+ },
1174
+ },
1175
+ // git_merge tool
1176
+ {
1177
+ name: 'git_merge',
1178
+ description: 'Merge one or more branches into the current branch',
1179
+ inputSchema: {
1180
+ type: 'object',
1181
+ properties: {
1182
+ path: {
1183
+ type: 'string',
1184
+ description: 'Path to the git repository',
1185
+ },
1186
+ branch: {
1187
+ type: 'string',
1188
+ description: 'Branch to merge into current branch',
1189
+ },
1190
+ noFf: {
1191
+ type: 'boolean',
1192
+ description: 'Create a merge commit even when fast-forward is possible',
1193
+ },
1194
+ squash: {
1195
+ type: 'boolean',
1196
+ description: 'Squash commits into a single commit',
1197
+ },
1198
+ },
1199
+ required: ['branch'],
1200
+ },
1201
+ handler: async (params) => {
1202
+ const { path, branch, noFf, squash } = params;
1203
+ const validatedPath = validatePath(path);
1204
+ const args = ['merge'];
1205
+ if (noFf)
1206
+ args.push('--no-ff');
1207
+ if (squash)
1208
+ args.push('--squash');
1209
+ args.push(validateBranchName(branch));
1210
+ return {
1211
+ content: [
1212
+ {
1213
+ type: 'text',
1214
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1215
+ },
1216
+ ],
1217
+ };
1218
+ },
1219
+ },
1220
+ // git_rebase tool
1221
+ {
1222
+ name: 'git_rebase',
1223
+ description: 'Reapply commits on top of another base tip',
1224
+ inputSchema: {
1225
+ type: 'object',
1226
+ properties: {
1227
+ path: {
1228
+ type: 'string',
1229
+ description: 'Path to the git repository',
1230
+ },
1231
+ onto: {
1232
+ type: 'string',
1233
+ description: 'Branch or commit to rebase onto',
1234
+ },
1235
+ abort: {
1236
+ type: 'boolean',
1237
+ description: 'Abort an in-progress rebase',
1238
+ },
1239
+ continue: {
1240
+ type: 'boolean',
1241
+ description: 'Continue an in-progress rebase',
1242
+ },
1243
+ },
1244
+ },
1245
+ handler: async (params) => {
1246
+ const { path, onto, abort, continue: cont } = params;
1247
+ const validatedPath = validatePath(path);
1248
+ const args = ['rebase'];
1249
+ if (abort)
1250
+ args.push('--abort');
1251
+ else if (cont)
1252
+ args.push('--continue');
1253
+ else if (onto)
1254
+ args.push(validateCommitRef(onto));
1255
+ return {
1256
+ content: [
1257
+ {
1258
+ type: 'text',
1259
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1260
+ },
1261
+ ],
1262
+ };
1263
+ },
1264
+ },
1265
+ // git_stash tool
1266
+ {
1267
+ name: 'git_stash',
1268
+ description: 'Stash the changes in a dirty working directory away',
1269
+ inputSchema: {
1270
+ type: 'object',
1271
+ properties: {
1272
+ path: {
1273
+ type: 'string',
1274
+ description: 'Path to the git repository',
1275
+ },
1276
+ action: {
1277
+ type: 'string',
1278
+ enum: ['push', 'pop', 'list', 'drop', 'apply', 'clear'],
1279
+ description: 'Stash action to perform',
1280
+ },
1281
+ message: {
1282
+ type: 'string',
1283
+ description: 'Message for the stash entry',
1284
+ },
1285
+ },
1286
+ },
1287
+ handler: async (params) => {
1288
+ const { path, action, message } = params;
1289
+ const validatedPath = validatePath(path);
1290
+ const args = ['stash'];
1291
+ if (action)
1292
+ args.push(action);
1293
+ if (message && action === 'push') {
1294
+ // Validate stash message for shell injection
1295
+ if (/[`$]/.test(message)) {
1296
+ throw new Error('Invalid stash message: contains forbidden characters');
1297
+ }
1298
+ args.push('-m', message);
1299
+ }
1300
+ return {
1301
+ content: [
1302
+ {
1303
+ type: 'text',
1304
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1305
+ },
1306
+ ],
1307
+ };
1308
+ },
1309
+ },
1310
+ // git_tag tool
1311
+ {
1312
+ name: 'git_tag',
1313
+ description: 'Create, list, delete, or verify tags in the repository',
1314
+ inputSchema: {
1315
+ type: 'object',
1316
+ properties: {
1317
+ path: {
1318
+ type: 'string',
1319
+ description: 'Path to the git repository',
1320
+ },
1321
+ name: {
1322
+ type: 'string',
1323
+ description: 'Name of the tag',
1324
+ },
1325
+ message: {
1326
+ type: 'string',
1327
+ description: 'Message for annotated tag',
1328
+ },
1329
+ delete: {
1330
+ type: 'boolean',
1331
+ description: 'Delete the specified tag',
1332
+ },
1333
+ },
1334
+ },
1335
+ handler: async (params) => {
1336
+ const { path, name, message, delete: del } = params;
1337
+ const validatedPath = validatePath(path);
1338
+ const args = ['tag'];
1339
+ if (del && name)
1340
+ args.push('-d', validateBranchName(name));
1341
+ else if (message && name) {
1342
+ // Validate tag message for shell injection
1343
+ if (/[`$]/.test(message)) {
1344
+ throw new Error('Invalid tag message: contains forbidden characters');
1345
+ }
1346
+ args.push('-a', validateBranchName(name), '-m', message);
1347
+ }
1348
+ else if (name)
1349
+ args.push(validateBranchName(name));
1350
+ return {
1351
+ content: [
1352
+ {
1353
+ type: 'text',
1354
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1355
+ },
1356
+ ],
1357
+ };
1358
+ },
1359
+ },
1360
+ // git_remote tool
1361
+ {
1362
+ name: 'git_remote',
1363
+ description: 'Manage set of tracked repositories (list, add, remove, update remotes)',
1364
+ inputSchema: {
1365
+ type: 'object',
1366
+ properties: {
1367
+ path: {
1368
+ type: 'string',
1369
+ description: 'Path to the git repository',
1370
+ },
1371
+ action: {
1372
+ type: 'string',
1373
+ enum: ['list', 'add', 'remove', 'rename', 'set-url'],
1374
+ description: 'Remote action to perform',
1375
+ },
1376
+ name: {
1377
+ type: 'string',
1378
+ description: 'Name of the remote',
1379
+ },
1380
+ url: {
1381
+ type: 'string',
1382
+ description: 'URL of the remote repository',
1383
+ },
1384
+ },
1385
+ },
1386
+ handler: async (params) => {
1387
+ const { path, action, name, url } = params;
1388
+ const validatedPath = validatePath(path);
1389
+ const args = ['remote'];
1390
+ if (action === 'list' || !action)
1391
+ args.push('-v');
1392
+ else if (action === 'add' && name && url)
1393
+ args.push('add', validateRemoteName(name), validateUrl(url));
1394
+ else if (action === 'remove' && name)
1395
+ args.push('remove', validateRemoteName(name));
1396
+ else if (action === 'set-url' && name && url)
1397
+ args.push('set-url', validateRemoteName(name), validateUrl(url));
1398
+ return {
1399
+ content: [
1400
+ {
1401
+ type: 'text',
1402
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1403
+ },
1404
+ ],
1405
+ };
1406
+ },
1407
+ },
1408
+ // git_fetch tool
1409
+ {
1410
+ name: 'git_fetch',
1411
+ description: 'Fetch branches and tags from one or more remote repositories',
1412
+ inputSchema: {
1413
+ type: 'object',
1414
+ properties: {
1415
+ path: {
1416
+ type: 'string',
1417
+ description: 'Path to the git repository',
1418
+ },
1419
+ remote: {
1420
+ type: 'string',
1421
+ description: 'Name of the remote to fetch from',
1422
+ },
1423
+ all: {
1424
+ type: 'boolean',
1425
+ description: 'Fetch all remotes',
1426
+ },
1427
+ prune: {
1428
+ type: 'boolean',
1429
+ description: 'Prune remote-tracking branches no longer on remote',
1430
+ },
1431
+ },
1432
+ },
1433
+ handler: async (params) => {
1434
+ const { path, remote, all, prune } = params;
1435
+ const validatedPath = validatePath(path);
1436
+ const args = ['fetch'];
1437
+ if (all)
1438
+ args.push('--all');
1439
+ if (prune)
1440
+ args.push('--prune');
1441
+ if (remote && !all)
1442
+ args.push(validateRemoteName(remote));
1443
+ return {
1444
+ content: [
1445
+ {
1446
+ type: 'text',
1447
+ text: `Executed: git ${args.join(' ')} in ${validatedPath === '.' ? 'current directory' : validatedPath}`,
1448
+ },
1449
+ ],
1450
+ };
1451
+ },
1452
+ },
1453
+ ];
1454
+ // Register all git tools in the registry on module load
1455
+ gitTools.forEach((tool) => {
1456
+ toolRegistry.set(tool.name, tool);
1457
+ });
1458
+ /**
1459
+ * Register a new tool in the registry
1460
+ * @param tool - The tool to register
1461
+ * @throws Error if tool with same name already exists or if handler is missing
1462
+ */
1463
+ export function registerTool(tool) {
1464
+ if (!tool.handler || typeof tool.handler !== 'function') {
1465
+ throw new Error(`Tool '${tool.name}' must have a handler function`);
1466
+ }
1467
+ if (toolRegistry.has(tool.name)) {
1468
+ throw new Error(`Tool with name '${tool.name}' already exists (duplicate)`);
1469
+ }
1470
+ toolRegistry.set(tool.name, tool);
1471
+ }
1472
+ /**
1473
+ * Validate input parameters against a tool's schema
1474
+ * @param tool - The tool whose schema to validate against
1475
+ * @param params - The parameters to validate
1476
+ * @returns Validation result with errors if any
1477
+ */
1478
+ export function validateToolInput(tool, params) {
1479
+ const errors = [];
1480
+ const schema = tool.inputSchema;
1481
+ // Check required parameters
1482
+ if (schema.required) {
1483
+ for (const requiredParam of schema.required) {
1484
+ if (!(requiredParam in params) || params[requiredParam] === undefined) {
1485
+ errors.push(`Missing required parameter: ${requiredParam}`);
1486
+ }
1487
+ }
1488
+ }
1489
+ // Check parameter types
1490
+ if (schema.properties) {
1491
+ for (const [key, value] of Object.entries(params)) {
1492
+ const propSchema = schema.properties[key];
1493
+ if (!propSchema) {
1494
+ // Unknown parameter - could be an error or we could ignore it
1495
+ continue;
1496
+ }
1497
+ // Type validation
1498
+ const valueType = Array.isArray(value) ? 'array' : typeof value;
1499
+ if (propSchema.type && valueType !== propSchema.type) {
1500
+ errors.push(`Parameter '${key}' has invalid type: expected ${propSchema.type}, got ${valueType}`);
1501
+ }
1502
+ // Enum validation
1503
+ if (propSchema.enum && !propSchema.enum.includes(value)) {
1504
+ errors.push(`Parameter '${key}' must be one of: ${propSchema.enum.join(', ')}`);
1505
+ }
1506
+ // Number constraints
1507
+ if (propSchema.type === 'number' && typeof value === 'number') {
1508
+ if (propSchema.minimum !== undefined && value < propSchema.minimum) {
1509
+ errors.push(`Parameter '${key}' must be at least ${propSchema.minimum}`);
1510
+ }
1511
+ if (propSchema.maximum !== undefined && value > propSchema.maximum) {
1512
+ errors.push(`Parameter '${key}' must be at most ${propSchema.maximum}`);
1513
+ }
1514
+ }
1515
+ // String pattern validation
1516
+ if (propSchema.type === 'string' && typeof value === 'string' && propSchema.pattern) {
1517
+ const regex = new RegExp(propSchema.pattern);
1518
+ if (!regex.test(value)) {
1519
+ errors.push(`Parameter '${key}' does not match required pattern: ${propSchema.pattern}`);
1520
+ }
1521
+ }
1522
+ // Array item type validation
1523
+ if (propSchema.type === 'array' && Array.isArray(value) && propSchema.items) {
1524
+ const itemType = propSchema.items.type;
1525
+ for (let i = 0; i < value.length; i++) {
1526
+ const itemValueType = typeof value[i];
1527
+ if (itemType && itemValueType !== itemType) {
1528
+ errors.push(`Array item at index ${i} in '${key}' has invalid type: expected ${itemType}, got ${itemValueType}`);
1529
+ }
1530
+ }
1531
+ }
1532
+ }
1533
+ }
1534
+ return {
1535
+ valid: errors.length === 0,
1536
+ errors,
1537
+ };
1538
+ }
1539
+ /**
1540
+ * Invoke a tool by name with the given parameters
1541
+ * @param toolName - Name of the tool to invoke
1542
+ * @param params - Parameters to pass to the tool
1543
+ * @returns Result of the tool invocation
1544
+ * @throws Error if tool not found
1545
+ */
1546
+ export async function invokeTool(toolName, params) {
1547
+ const tool = toolRegistry.get(toolName);
1548
+ if (!tool) {
1549
+ throw new Error(`Tool '${toolName}' not found (does not exist)`);
1550
+ }
1551
+ // Validate parameters before invoking
1552
+ const validation = validateToolInput(tool, params);
1553
+ if (!validation.valid) {
1554
+ return {
1555
+ content: [
1556
+ {
1557
+ type: 'text',
1558
+ text: `Validation error: ${validation.errors.join('; ')}`,
1559
+ },
1560
+ ],
1561
+ isError: true,
1562
+ };
1563
+ }
1564
+ // Invoke the handler with error handling
1565
+ try {
1566
+ return await tool.handler(params);
1567
+ }
1568
+ catch (error) {
1569
+ return {
1570
+ content: [
1571
+ {
1572
+ type: 'text',
1573
+ text: error instanceof Error ? error.message : String(error),
1574
+ },
1575
+ ],
1576
+ isError: true,
1577
+ };
1578
+ }
1579
+ }
1580
+ /**
1581
+ * Get a list of all registered tools
1582
+ * @returns Array of tool definitions (without handlers)
1583
+ */
1584
+ export function listTools() {
1585
+ const tools = [];
1586
+ for (const tool of toolRegistry.values()) {
1587
+ // Return tool without handler
1588
+ tools.push({
1589
+ name: tool.name,
1590
+ description: tool.description,
1591
+ inputSchema: tool.inputSchema,
1592
+ });
1593
+ }
1594
+ return tools;
1595
+ }
1596
+ /**
1597
+ * Get a tool by name
1598
+ * @param name - Name of the tool to retrieve
1599
+ * @returns The tool if found, undefined otherwise
1600
+ */
1601
+ export function getTool(name) {
1602
+ return toolRegistry.get(name);
1603
+ }
1604
+ //# sourceMappingURL=tools.js.map