@andrebuzeli/git-mcp 5.4.8 → 5.4.10
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.
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +9 -3
- package/dist/server.js.map +1 -1
- package/dist/tools/git-history.d.ts +259 -0
- package/dist/tools/git-history.d.ts.map +1 -0
- package/dist/tools/git-history.js +829 -0
- package/dist/tools/git-history.js.map +1 -0
- package/dist/utils/parameter-validator.d.ts.map +1 -1
- package/dist/utils/parameter-validator.js +4 -2
- package/dist/utils/parameter-validator.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Git History Tool
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive change tracking tool that records all repository modifications
|
|
6
|
+
* with detailed timestamps, storing history locally in JSON and syncing to remote providers.
|
|
7
|
+
*
|
|
8
|
+
* Operations: log, track, sync, export, auto
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.GitHistoryTool = void 0;
|
|
45
|
+
const git_command_executor_js_1 = require("../utils/git-command-executor.js");
|
|
46
|
+
const parameter_validator_js_1 = require("../utils/parameter-validator.js");
|
|
47
|
+
const operation_error_handler_js_1 = require("../utils/operation-error-handler.js");
|
|
48
|
+
const provider_operation_handler_js_1 = require("../providers/provider-operation-handler.js");
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
class GitHistoryTool {
|
|
52
|
+
gitExecutor;
|
|
53
|
+
providerHandler;
|
|
54
|
+
historyPath;
|
|
55
|
+
configPath;
|
|
56
|
+
constructor(providerConfig) {
|
|
57
|
+
this.gitExecutor = new git_command_executor_js_1.GitCommandExecutor();
|
|
58
|
+
this.historyPath = path.join('.git-history', 'history.json');
|
|
59
|
+
this.configPath = path.join('.git-history', 'config.json');
|
|
60
|
+
if (providerConfig) {
|
|
61
|
+
this.providerHandler = new provider_operation_handler_js_1.ProviderOperationHandler(providerConfig);
|
|
62
|
+
}
|
|
63
|
+
this.ensureHistoryDirectory();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Execute git-history operation
|
|
67
|
+
*/
|
|
68
|
+
async execute(params) {
|
|
69
|
+
const startTime = Date.now();
|
|
70
|
+
try {
|
|
71
|
+
// Validate basic parameters
|
|
72
|
+
const validation = parameter_validator_js_1.ParameterValidator.validateToolParams('git-history', params);
|
|
73
|
+
if (!validation.isValid) {
|
|
74
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('VALIDATION_ERROR', `Parameter validation failed: ${validation.errors.join(', ')}`, params.action, { validationErrors: validation.errors }, validation.suggestions);
|
|
75
|
+
}
|
|
76
|
+
// Validate operation-specific parameters
|
|
77
|
+
const operationValidation = this.validateOperationParams(params);
|
|
78
|
+
if (!operationValidation.isValid) {
|
|
79
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('VALIDATION_ERROR', `Operation validation failed: ${operationValidation.errors.join(', ')}`, params.action, { validationErrors: operationValidation.errors }, operationValidation.suggestions);
|
|
80
|
+
}
|
|
81
|
+
// Route to appropriate handler
|
|
82
|
+
switch (params.action) {
|
|
83
|
+
case 'log':
|
|
84
|
+
return await this.handleLog(params, startTime);
|
|
85
|
+
case 'track':
|
|
86
|
+
return await this.handleTrack(params, startTime);
|
|
87
|
+
case 'sync':
|
|
88
|
+
return await this.handleSync(params, startTime);
|
|
89
|
+
case 'export':
|
|
90
|
+
return await this.handleExport(params, startTime);
|
|
91
|
+
case 'auto':
|
|
92
|
+
return await this.handleAuto(params, startTime);
|
|
93
|
+
default:
|
|
94
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('UNSUPPORTED_OPERATION', `Unsupported operation: ${params.action}`, params.action, { supportedOperations: ['log', 'track', 'sync', 'export', 'auto'] }, ['Use one of the supported operations: log, track, sync, export, auto']);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
99
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('EXECUTION_ERROR', `Failed to execute git-history operation: ${errorMessage}`, params.action, { error: errorMessage });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate operation-specific parameters
|
|
104
|
+
*/
|
|
105
|
+
validateOperationParams(params) {
|
|
106
|
+
const errors = [];
|
|
107
|
+
const suggestions = [];
|
|
108
|
+
switch (params.action) {
|
|
109
|
+
case 'track':
|
|
110
|
+
if (!params.message) {
|
|
111
|
+
errors.push('Message is required for track operation');
|
|
112
|
+
suggestions.push('Provide a message describing the change');
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case 'sync':
|
|
116
|
+
if (!this.providerHandler && params.syncMethod === 'api') {
|
|
117
|
+
errors.push('Provider configuration required for API sync');
|
|
118
|
+
suggestions.push('Configure GitHub/Gitea provider or use file sync method');
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case 'export':
|
|
122
|
+
if (params.outputPath && !path.isAbsolute(params.outputPath)) {
|
|
123
|
+
errors.push('Output path must be absolute');
|
|
124
|
+
suggestions.push('Provide an absolute path for export file');
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
isValid: errors.length === 0,
|
|
130
|
+
errors,
|
|
131
|
+
suggestions
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Handle log operation - View history entries
|
|
136
|
+
*/
|
|
137
|
+
async handleLog(params, startTime) {
|
|
138
|
+
try {
|
|
139
|
+
const history = await this.loadHistory();
|
|
140
|
+
let entries = [...history.entries];
|
|
141
|
+
// Apply filters
|
|
142
|
+
if (params.since) {
|
|
143
|
+
const sinceDate = new Date(params.since);
|
|
144
|
+
entries = entries.filter(entry => new Date(entry.timestamp) >= sinceDate);
|
|
145
|
+
}
|
|
146
|
+
if (params.until) {
|
|
147
|
+
const untilDate = new Date(params.until);
|
|
148
|
+
entries = entries.filter(entry => new Date(entry.timestamp) <= untilDate);
|
|
149
|
+
}
|
|
150
|
+
if (params.author) {
|
|
151
|
+
entries = entries.filter(entry => entry.author.toLowerCase().includes(params.author.toLowerCase()) ||
|
|
152
|
+
entry.authorEmail.toLowerCase().includes(params.author.toLowerCase()));
|
|
153
|
+
}
|
|
154
|
+
if (params.filePath) {
|
|
155
|
+
entries = entries.filter(entry => entry.filesChanged.some(file => file.path.includes(params.filePath)));
|
|
156
|
+
}
|
|
157
|
+
if (params.branch) {
|
|
158
|
+
entries = entries.filter(entry => entry.branch === params.branch);
|
|
159
|
+
}
|
|
160
|
+
// Sort by timestamp (newest first)
|
|
161
|
+
entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
162
|
+
// Apply limit
|
|
163
|
+
const limit = params.limit || 50;
|
|
164
|
+
entries = entries.slice(0, limit);
|
|
165
|
+
// Format output
|
|
166
|
+
const format = params.format || 'json';
|
|
167
|
+
let formattedData;
|
|
168
|
+
if (format === 'markdown') {
|
|
169
|
+
formattedData = this.formatHistoryAsMarkdown(entries);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
formattedData = {
|
|
173
|
+
totalEntries: history.entries.length,
|
|
174
|
+
filteredEntries: entries.length,
|
|
175
|
+
entries: entries
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
data: {
|
|
181
|
+
...formattedData,
|
|
182
|
+
filters: {
|
|
183
|
+
since: params.since,
|
|
184
|
+
until: params.until,
|
|
185
|
+
author: params.author,
|
|
186
|
+
filePath: params.filePath,
|
|
187
|
+
branch: params.branch,
|
|
188
|
+
limit: limit
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
metadata: {
|
|
192
|
+
operation: params.action,
|
|
193
|
+
executionTime: Date.now() - startTime,
|
|
194
|
+
timestamp: new Date().toISOString()
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
200
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('LOG_FAILED', `Failed to retrieve history: ${errorMessage}`, params.action, { error: errorMessage });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Handle track operation - Manually record a change
|
|
205
|
+
*/
|
|
206
|
+
async handleTrack(params, startTime) {
|
|
207
|
+
try {
|
|
208
|
+
const currentBranch = await this.getCurrentBranch(params.projectPath);
|
|
209
|
+
const timestamp = params.timestamp || new Date().toISOString();
|
|
210
|
+
const entry = {
|
|
211
|
+
id: `manual-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
212
|
+
timestamp,
|
|
213
|
+
author: await this.getProviderUserName() || 'Unknown',
|
|
214
|
+
authorEmail: await this.getGitUserEmail() || '',
|
|
215
|
+
message: params.message,
|
|
216
|
+
filesChanged: params.files ? params.files.map(file => ({
|
|
217
|
+
path: file,
|
|
218
|
+
status: 'modified',
|
|
219
|
+
additions: params.additions || 0,
|
|
220
|
+
deletions: params.deletions || 0
|
|
221
|
+
})) : [],
|
|
222
|
+
additions: params.additions || 0,
|
|
223
|
+
deletions: params.deletions || 0,
|
|
224
|
+
branch: currentBranch || 'main',
|
|
225
|
+
manual: true,
|
|
226
|
+
synced: false
|
|
227
|
+
};
|
|
228
|
+
await this.addHistoryEntry(entry);
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
data: {
|
|
232
|
+
entry: entry,
|
|
233
|
+
message: 'Change tracked successfully'
|
|
234
|
+
},
|
|
235
|
+
metadata: {
|
|
236
|
+
operation: params.action,
|
|
237
|
+
executionTime: Date.now() - startTime,
|
|
238
|
+
timestamp: new Date().toISOString()
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
244
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('TRACK_FAILED', `Failed to track change: ${errorMessage}`, params.action, { error: errorMessage });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Handle sync operation - Sync local history to remote
|
|
249
|
+
*/
|
|
250
|
+
async handleSync(params, startTime) {
|
|
251
|
+
try {
|
|
252
|
+
const history = await this.loadHistory();
|
|
253
|
+
const unsyncedEntries = history.entries.filter(entry => !entry.synced);
|
|
254
|
+
if (unsyncedEntries.length === 0) {
|
|
255
|
+
return {
|
|
256
|
+
success: true,
|
|
257
|
+
data: {
|
|
258
|
+
message: 'All entries are already synced',
|
|
259
|
+
totalEntries: history.entries.length,
|
|
260
|
+
syncedEntries: history.entries.length - unsyncedEntries.length,
|
|
261
|
+
unsyncedEntries: 0
|
|
262
|
+
},
|
|
263
|
+
metadata: {
|
|
264
|
+
operation: params.action,
|
|
265
|
+
executionTime: Date.now() - startTime,
|
|
266
|
+
timestamp: new Date().toISOString()
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const syncMethod = params.syncMethod || 'file';
|
|
271
|
+
let syncResult = {};
|
|
272
|
+
if (syncMethod === 'file') {
|
|
273
|
+
syncResult = await this.syncViaFile(params, unsyncedEntries);
|
|
274
|
+
}
|
|
275
|
+
else if (syncMethod === 'api') {
|
|
276
|
+
syncResult = await this.syncViaApi(params, unsyncedEntries);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('INVALID_SYNC_METHOD', `Invalid sync method: ${syncMethod}`, params.action, { supportedMethods: ['file', 'api'] }, ['Use "file" to commit history.json or "api" to use provider API']);
|
|
280
|
+
}
|
|
281
|
+
// Mark entries as synced
|
|
282
|
+
const updatedHistory = await this.loadHistory();
|
|
283
|
+
updatedHistory.entries.forEach(entry => {
|
|
284
|
+
if (!entry.synced && unsyncedEntries.some(u => u.id === entry.id)) {
|
|
285
|
+
entry.synced = true;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
await this.saveHistory(updatedHistory);
|
|
289
|
+
return {
|
|
290
|
+
success: true,
|
|
291
|
+
data: {
|
|
292
|
+
...syncResult,
|
|
293
|
+
syncedEntries: unsyncedEntries.length,
|
|
294
|
+
totalEntries: history.entries.length,
|
|
295
|
+
syncMethod
|
|
296
|
+
},
|
|
297
|
+
metadata: {
|
|
298
|
+
operation: params.action,
|
|
299
|
+
executionTime: Date.now() - startTime,
|
|
300
|
+
timestamp: new Date().toISOString()
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
306
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('SYNC_FAILED', `Failed to sync history: ${errorMessage}`, params.action, { error: errorMessage });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Handle export operation - Export history to file
|
|
311
|
+
*/
|
|
312
|
+
async handleExport(params, startTime) {
|
|
313
|
+
try {
|
|
314
|
+
const history = await this.loadHistory();
|
|
315
|
+
const outputPath = params.outputPath || path.join(params.projectPath, 'HISTORY.json');
|
|
316
|
+
let exportData;
|
|
317
|
+
if (path.extname(outputPath).toLowerCase() === '.md') {
|
|
318
|
+
// Export as Markdown
|
|
319
|
+
exportData = this.formatHistoryAsMarkdown(history.entries);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Export as JSON
|
|
323
|
+
exportData = {
|
|
324
|
+
exportedAt: new Date().toISOString(),
|
|
325
|
+
totalEntries: history.entries.length,
|
|
326
|
+
includeDiffs: params.includeDiffs || false,
|
|
327
|
+
entries: params.includeDiffs ? history.entries : history.entries.map(entry => {
|
|
328
|
+
const { diff, ...entryWithoutDiff } = entry;
|
|
329
|
+
return entryWithoutDiff;
|
|
330
|
+
})
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// Write to file
|
|
334
|
+
const outputDir = path.dirname(outputPath);
|
|
335
|
+
if (!fs.existsSync(outputDir)) {
|
|
336
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
337
|
+
}
|
|
338
|
+
if (typeof exportData === 'string') {
|
|
339
|
+
fs.writeFileSync(outputPath, exportData, 'utf8');
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
fs.writeFileSync(outputPath, JSON.stringify(exportData, null, 2), 'utf8');
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
success: true,
|
|
346
|
+
data: {
|
|
347
|
+
exportedPath: outputPath,
|
|
348
|
+
format: path.extname(outputPath).toLowerCase() === '.md' ? 'markdown' : 'json',
|
|
349
|
+
totalEntries: history.entries.length,
|
|
350
|
+
includeDiffs: params.includeDiffs || false
|
|
351
|
+
},
|
|
352
|
+
metadata: {
|
|
353
|
+
operation: params.action,
|
|
354
|
+
executionTime: Date.now() - startTime,
|
|
355
|
+
timestamp: new Date().toISOString()
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
361
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('EXPORT_FAILED', `Failed to export history: ${errorMessage}`, params.action, { error: errorMessage });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Handle auto operation - Enable/disable automatic tracking
|
|
366
|
+
*/
|
|
367
|
+
async handleAuto(params, startTime) {
|
|
368
|
+
try {
|
|
369
|
+
const config = await this.loadConfig();
|
|
370
|
+
const wasEnabled = config.autoTracking;
|
|
371
|
+
if (params.enabled !== undefined) {
|
|
372
|
+
config.autoTracking = params.enabled;
|
|
373
|
+
await this.saveConfig(config);
|
|
374
|
+
if (params.enabled && !wasEnabled) {
|
|
375
|
+
// Enable auto-tracking - sync current commits
|
|
376
|
+
await this.syncRecentCommits();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
success: true,
|
|
381
|
+
data: {
|
|
382
|
+
autoTrackingEnabled: config.autoTracking,
|
|
383
|
+
message: `Auto-tracking ${config.autoTracking ? 'enabled' : 'disabled'}`
|
|
384
|
+
},
|
|
385
|
+
metadata: {
|
|
386
|
+
operation: params.action,
|
|
387
|
+
executionTime: Date.now() - startTime,
|
|
388
|
+
timestamp: new Date().toISOString()
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
394
|
+
return operation_error_handler_js_1.OperationErrorHandler.createToolError('AUTO_CONFIG_FAILED', `Failed to configure auto-tracking: ${errorMessage}`, params.action, { error: errorMessage });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Sync via file commit
|
|
399
|
+
*/
|
|
400
|
+
async syncViaFile(params, entries) {
|
|
401
|
+
try {
|
|
402
|
+
const historyFile = path.join(params.projectPath, 'HISTORY.json');
|
|
403
|
+
// Write history to repository
|
|
404
|
+
const historyData = {
|
|
405
|
+
lastUpdated: new Date().toISOString(),
|
|
406
|
+
entries: entries
|
|
407
|
+
};
|
|
408
|
+
fs.writeFileSync(historyFile, JSON.stringify(historyData, null, 2), 'utf8');
|
|
409
|
+
// Commit the file
|
|
410
|
+
const addResult = await this.gitExecutor.executeGitCommand('add', ['HISTORY.json'], params.projectPath);
|
|
411
|
+
if (!addResult.success) {
|
|
412
|
+
throw new Error(`Failed to add HISTORY.json: ${addResult.stderr}`);
|
|
413
|
+
}
|
|
414
|
+
const commitResult = await this.gitExecutor.executeGitCommand('commit', ['-m', `Update history: ${entries.length} new entries`], params.projectPath);
|
|
415
|
+
if (!commitResult.success && !commitResult.stderr.includes('nothing to commit')) {
|
|
416
|
+
throw new Error(`Failed to commit HISTORY.json: ${commitResult.stderr}`);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
method: 'file',
|
|
420
|
+
committed: commitResult.success,
|
|
421
|
+
message: 'History synced via file commit'
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
throw new Error(`File sync failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Sync via provider API
|
|
430
|
+
*/
|
|
431
|
+
async syncViaApi(params, entries) {
|
|
432
|
+
if (!this.providerHandler) {
|
|
433
|
+
throw new Error('Provider handler not configured');
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const provider = params.provider || 'github';
|
|
437
|
+
const repo = params.repo || await this.getRepoName();
|
|
438
|
+
// Create a summary entry for the API
|
|
439
|
+
const summary = {
|
|
440
|
+
period: `${entries[0]?.timestamp} to ${entries[entries.length - 1]?.timestamp}`,
|
|
441
|
+
totalEntries: entries.length,
|
|
442
|
+
authors: [...new Set(entries.map(e => e.author))],
|
|
443
|
+
filesChanged: entries.reduce((sum, e) => sum + e.filesChanged.length, 0),
|
|
444
|
+
totalAdditions: entries.reduce((sum, e) => sum + e.additions, 0),
|
|
445
|
+
totalDeletions: entries.reduce((sum, e) => sum + e.deletions, 0),
|
|
446
|
+
entries: entries.slice(0, 10) // Include first 10 entries in detail
|
|
447
|
+
};
|
|
448
|
+
const operation = {
|
|
449
|
+
provider,
|
|
450
|
+
operation: 'create',
|
|
451
|
+
parameters: {
|
|
452
|
+
repo,
|
|
453
|
+
title: `History Update: ${entries.length} changes`,
|
|
454
|
+
body: JSON.stringify(summary, null, 2),
|
|
455
|
+
type: 'issue' // Create as issue for tracking
|
|
456
|
+
},
|
|
457
|
+
requiresAuth: true,
|
|
458
|
+
isRemoteOperation: true
|
|
459
|
+
};
|
|
460
|
+
const result = await this.providerHandler.executeOperation(operation);
|
|
461
|
+
return {
|
|
462
|
+
method: 'api',
|
|
463
|
+
provider,
|
|
464
|
+
success: result.success,
|
|
465
|
+
message: result.success ? 'History synced via API' : 'API sync failed'
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
throw new Error(`API sync failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Format history as Markdown
|
|
474
|
+
*/
|
|
475
|
+
formatHistoryAsMarkdown(entries) {
|
|
476
|
+
let markdown = '# Repository History\n\n';
|
|
477
|
+
markdown += `Generated on: ${new Date().toISOString()}\n\n`;
|
|
478
|
+
markdown += `Total entries: ${entries.length}\n\n`;
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
markdown += `## ${entry.message}\n\n`;
|
|
481
|
+
markdown += `- **Date:** ${new Date(entry.timestamp).toLocaleString()}\n`;
|
|
482
|
+
markdown += `- **Author:** ${entry.author} <${entry.authorEmail}>\n`;
|
|
483
|
+
markdown += `- **Branch:** ${entry.branch}\n`;
|
|
484
|
+
markdown += `- **Commit:** ${entry.commitHash || 'Manual entry'}\n`;
|
|
485
|
+
markdown += `- **Changes:** +${entry.additions} -${entry.deletions}\n`;
|
|
486
|
+
if (entry.filesChanged.length > 0) {
|
|
487
|
+
markdown += `- **Files changed:**\n`;
|
|
488
|
+
for (const file of entry.filesChanged) {
|
|
489
|
+
markdown += ` - ${file.status.toUpperCase()}: ${file.path} (+${file.additions} -${file.deletions})\n`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
493
|
+
markdown += `- **Tags:** ${entry.tags.join(', ')}\n`;
|
|
494
|
+
}
|
|
495
|
+
markdown += '\n---\n\n';
|
|
496
|
+
}
|
|
497
|
+
return markdown;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Load history from file
|
|
501
|
+
*/
|
|
502
|
+
async loadHistory() {
|
|
503
|
+
try {
|
|
504
|
+
if (fs.existsSync(this.historyPath)) {
|
|
505
|
+
const data = fs.readFileSync(this.historyPath, 'utf8');
|
|
506
|
+
return JSON.parse(data);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
console.warn('Failed to load history file:', error);
|
|
511
|
+
}
|
|
512
|
+
return { entries: [] };
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Save history to file
|
|
516
|
+
*/
|
|
517
|
+
async saveHistory(history) {
|
|
518
|
+
try {
|
|
519
|
+
fs.writeFileSync(this.historyPath, JSON.stringify(history, null, 2), 'utf8');
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
throw new Error(`Failed to save history: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Add entry to history
|
|
527
|
+
*/
|
|
528
|
+
async addHistoryEntry(entry) {
|
|
529
|
+
const history = await this.loadHistory();
|
|
530
|
+
history.entries.push(entry);
|
|
531
|
+
await this.saveHistory(history);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Load configuration
|
|
535
|
+
*/
|
|
536
|
+
async loadConfig() {
|
|
537
|
+
try {
|
|
538
|
+
if (fs.existsSync(this.configPath)) {
|
|
539
|
+
const data = fs.readFileSync(this.configPath, 'utf8');
|
|
540
|
+
return JSON.parse(data);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
console.warn('Failed to load config file:', error);
|
|
545
|
+
}
|
|
546
|
+
return { autoTracking: false };
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Save configuration
|
|
550
|
+
*/
|
|
551
|
+
async saveConfig(config) {
|
|
552
|
+
try {
|
|
553
|
+
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
throw new Error(`Failed to save config: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Sync recent commits to history
|
|
561
|
+
*/
|
|
562
|
+
async syncRecentCommits() {
|
|
563
|
+
try {
|
|
564
|
+
const config = await this.loadConfig();
|
|
565
|
+
const lastHash = config.lastCommitHash;
|
|
566
|
+
// Get commits since last sync
|
|
567
|
+
const logArgs = ['--oneline', '--numstat', '--format=format:%H|%an|%ae|%ad|%s'];
|
|
568
|
+
if (lastHash) {
|
|
569
|
+
logArgs.push(`${lastHash}..HEAD`);
|
|
570
|
+
}
|
|
571
|
+
const logResult = await this.gitExecutor.executeGitCommand('log', logArgs);
|
|
572
|
+
if (!logResult.success) {
|
|
573
|
+
console.warn('Failed to get git log:', logResult.stderr);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const lines = logResult.stdout.split('\n').filter(line => line.trim());
|
|
577
|
+
let currentEntry = null;
|
|
578
|
+
for (const line of lines) {
|
|
579
|
+
if (line.includes('|')) {
|
|
580
|
+
// Commit header line
|
|
581
|
+
if (currentEntry) {
|
|
582
|
+
await this.addHistoryEntry(currentEntry);
|
|
583
|
+
}
|
|
584
|
+
const [hash, gitAuthor, email, date, ...messageParts] = line.split('|');
|
|
585
|
+
const message = messageParts.join('|');
|
|
586
|
+
currentEntry = {
|
|
587
|
+
id: `commit-${hash}`,
|
|
588
|
+
timestamp: new Date(date).toISOString(),
|
|
589
|
+
commitHash: hash,
|
|
590
|
+
author: await this.getProviderUserName() || gitAuthor, // Use provider username, fallback to git author
|
|
591
|
+
authorEmail: email,
|
|
592
|
+
message,
|
|
593
|
+
filesChanged: [],
|
|
594
|
+
additions: 0,
|
|
595
|
+
deletions: 0,
|
|
596
|
+
branch: await this.getCurrentBranch() || 'main',
|
|
597
|
+
synced: false
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
else if (line.trim() && currentEntry) {
|
|
601
|
+
// File change line (from --numstat)
|
|
602
|
+
const parts = line.trim().split('\t');
|
|
603
|
+
if (parts.length >= 3) {
|
|
604
|
+
const additions = parseInt(parts[0]) || 0;
|
|
605
|
+
const deletions = parseInt(parts[1]) || 0;
|
|
606
|
+
const filePath = parts[2];
|
|
607
|
+
currentEntry.filesChanged.push({
|
|
608
|
+
path: filePath,
|
|
609
|
+
status: additions === 0 && deletions === 0 ? 'modified' : (additions > 0 ? 'added' : 'deleted'),
|
|
610
|
+
additions,
|
|
611
|
+
deletions
|
|
612
|
+
});
|
|
613
|
+
currentEntry.additions += additions;
|
|
614
|
+
currentEntry.deletions += deletions;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Add the last entry
|
|
619
|
+
if (currentEntry) {
|
|
620
|
+
await this.addHistoryEntry(currentEntry);
|
|
621
|
+
}
|
|
622
|
+
// Update last commit hash
|
|
623
|
+
const latestHash = await this.getLatestCommitHash();
|
|
624
|
+
if (latestHash) {
|
|
625
|
+
config.lastCommitHash = latestHash;
|
|
626
|
+
config.lastSyncTimestamp = new Date().toISOString();
|
|
627
|
+
await this.saveConfig(config);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
console.warn('Failed to sync recent commits:', error);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Get current branch
|
|
636
|
+
*/
|
|
637
|
+
async getCurrentBranch(projectPath) {
|
|
638
|
+
try {
|
|
639
|
+
const result = await this.gitExecutor.executeGitCommand('branch', ['--show-current'], projectPath);
|
|
640
|
+
return result.success ? result.stdout.trim() : null;
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get provider username (GitHub or Gitea username from env)
|
|
648
|
+
*/
|
|
649
|
+
async getProviderUserName() {
|
|
650
|
+
// Try GitHub username first
|
|
651
|
+
const githubUser = process.env.GITHUB_USERNAME;
|
|
652
|
+
if (githubUser)
|
|
653
|
+
return githubUser;
|
|
654
|
+
// Try Gitea username
|
|
655
|
+
const giteaUser = process.env.GITEA_USERNAME;
|
|
656
|
+
if (giteaUser)
|
|
657
|
+
return giteaUser;
|
|
658
|
+
// Fallback to git config user name
|
|
659
|
+
try {
|
|
660
|
+
const result = await this.gitExecutor.executeGitCommand('config', ['user.name']);
|
|
661
|
+
return result.success ? result.stdout.trim() : null;
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Get git user email
|
|
669
|
+
*/
|
|
670
|
+
async getGitUserEmail() {
|
|
671
|
+
try {
|
|
672
|
+
const result = await this.gitExecutor.executeGitCommand('config', ['user.email']);
|
|
673
|
+
return result.success ? result.stdout.trim() : null;
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get latest commit hash
|
|
681
|
+
*/
|
|
682
|
+
async getLatestCommitHash() {
|
|
683
|
+
try {
|
|
684
|
+
const result = await this.gitExecutor.executeGitCommand('rev-parse', ['HEAD']);
|
|
685
|
+
return result.success ? result.stdout.trim() : null;
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Get repository name from git config
|
|
693
|
+
*/
|
|
694
|
+
async getRepoName() {
|
|
695
|
+
try {
|
|
696
|
+
const result = await this.gitExecutor.executeGitCommand('config', ['--get', 'remote.origin.url']);
|
|
697
|
+
if (result.success) {
|
|
698
|
+
const url = result.stdout.trim();
|
|
699
|
+
// Extract repo name from URL (supports GitHub and Gitea formats)
|
|
700
|
+
const match = url.match(/\/([^\/]+?)(?:\.git)?$/);
|
|
701
|
+
return match ? match[1] : null;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// Ignore errors
|
|
706
|
+
}
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Ensure history directory exists
|
|
711
|
+
*/
|
|
712
|
+
ensureHistoryDirectory() {
|
|
713
|
+
try {
|
|
714
|
+
const dir = path.dirname(this.historyPath);
|
|
715
|
+
if (!fs.existsSync(dir)) {
|
|
716
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
console.warn('Failed to create history directory:', error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Get tool schema for MCP registration
|
|
725
|
+
*/
|
|
726
|
+
static getToolSchema() {
|
|
727
|
+
return {
|
|
728
|
+
name: 'git-history',
|
|
729
|
+
description: 'Comprehensive change tracking tool that records all repository modifications with detailed timestamps, storing history locally in JSON and syncing to remote providers.',
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
properties: {
|
|
733
|
+
action: {
|
|
734
|
+
type: 'string',
|
|
735
|
+
enum: ['log', 'track', 'sync', 'export', 'auto'],
|
|
736
|
+
description: 'The git-history operation to perform'
|
|
737
|
+
},
|
|
738
|
+
projectPath: {
|
|
739
|
+
type: 'string',
|
|
740
|
+
description: 'Absolute path to the project directory'
|
|
741
|
+
},
|
|
742
|
+
limit: {
|
|
743
|
+
type: 'number',
|
|
744
|
+
description: 'Number of entries to show (default: 50)',
|
|
745
|
+
minimum: 1,
|
|
746
|
+
maximum: 1000
|
|
747
|
+
},
|
|
748
|
+
since: {
|
|
749
|
+
type: 'string',
|
|
750
|
+
description: 'Start date filter (ISO 8601 format)'
|
|
751
|
+
},
|
|
752
|
+
until: {
|
|
753
|
+
type: 'string',
|
|
754
|
+
description: 'End date filter (ISO 8601 format)'
|
|
755
|
+
},
|
|
756
|
+
author: {
|
|
757
|
+
type: 'string',
|
|
758
|
+
description: 'Filter by author name or email'
|
|
759
|
+
},
|
|
760
|
+
filePath: {
|
|
761
|
+
type: 'string',
|
|
762
|
+
description: 'Filter by file path'
|
|
763
|
+
},
|
|
764
|
+
branch: {
|
|
765
|
+
type: 'string',
|
|
766
|
+
description: 'Filter by branch name'
|
|
767
|
+
},
|
|
768
|
+
format: {
|
|
769
|
+
type: 'string',
|
|
770
|
+
enum: ['json', 'markdown'],
|
|
771
|
+
description: 'Output format for log operation'
|
|
772
|
+
},
|
|
773
|
+
message: {
|
|
774
|
+
type: 'string',
|
|
775
|
+
description: 'Change description (required for track operation)'
|
|
776
|
+
},
|
|
777
|
+
timestamp: {
|
|
778
|
+
type: 'string',
|
|
779
|
+
description: 'Custom timestamp for track operation (ISO 8601 format)'
|
|
780
|
+
},
|
|
781
|
+
files: {
|
|
782
|
+
type: 'array',
|
|
783
|
+
items: { type: 'string' },
|
|
784
|
+
description: 'Files affected by the change (for track operation)'
|
|
785
|
+
},
|
|
786
|
+
additions: {
|
|
787
|
+
type: 'number',
|
|
788
|
+
description: 'Lines added (for track operation)',
|
|
789
|
+
minimum: 0
|
|
790
|
+
},
|
|
791
|
+
deletions: {
|
|
792
|
+
type: 'number',
|
|
793
|
+
description: 'Lines deleted (for track operation)',
|
|
794
|
+
minimum: 0
|
|
795
|
+
},
|
|
796
|
+
provider: {
|
|
797
|
+
type: 'string',
|
|
798
|
+
enum: ['github', 'gitea', 'both'],
|
|
799
|
+
description: 'Provider for remote sync operations'
|
|
800
|
+
},
|
|
801
|
+
syncMethod: {
|
|
802
|
+
type: 'string',
|
|
803
|
+
enum: ['file', 'api'],
|
|
804
|
+
description: 'Sync method: file (commit to repo) or api (use provider API)'
|
|
805
|
+
},
|
|
806
|
+
repo: {
|
|
807
|
+
type: 'string',
|
|
808
|
+
description: 'Repository name for remote sync'
|
|
809
|
+
},
|
|
810
|
+
outputPath: {
|
|
811
|
+
type: 'string',
|
|
812
|
+
description: 'Export file path (absolute path required)'
|
|
813
|
+
},
|
|
814
|
+
includeDiffs: {
|
|
815
|
+
type: 'boolean',
|
|
816
|
+
description: 'Include full diffs in export (default: false)'
|
|
817
|
+
},
|
|
818
|
+
enabled: {
|
|
819
|
+
type: 'boolean',
|
|
820
|
+
description: 'Enable/disable auto-tracking (for auto operation)'
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
required: ['action', 'projectPath']
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
exports.GitHistoryTool = GitHistoryTool;
|
|
829
|
+
//# sourceMappingURL=git-history.js.map
|