ronds-mcp-tracker 0.1.5 → 0.1.7
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/config.js +0 -1
- package/dist/server.js +199 -11
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -9,7 +9,6 @@ export const FAILED_QUEUE_CONFIG = {
|
|
|
9
9
|
};
|
|
10
10
|
function resolveWorkerId() {
|
|
11
11
|
const localConfigPath = path.resolve(process.cwd(), '.ai_config', 'config.json');
|
|
12
|
-
console.log('[mcp-tracker] localConfigPath:', localConfigPath);
|
|
13
12
|
try {
|
|
14
13
|
if (fs.existsSync(localConfigPath)) {
|
|
15
14
|
const raw = fs.readFileSync(localConfigPath, 'utf-8');
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { diffLines } from 'diff';
|
|
6
7
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -10,8 +11,57 @@ import { publishToKafka, initProducer, closeProducer } from './kafka.js';
|
|
|
10
11
|
import { loadConfig } from './config.js';
|
|
11
12
|
const MAX_ADDED_LINES = 10000;
|
|
12
13
|
const ADDED_TRUNCATED_MSG = '\n\n... (content truncated due to size) ...\n';
|
|
14
|
+
function extractRepoNameFromOriginUrl(url) {
|
|
15
|
+
const normalizedUrl = url.trim().replace(/\/$/, '');
|
|
16
|
+
if (!normalizedUrl) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
const lastSegment = normalizedUrl.split('/').pop() ?? '';
|
|
20
|
+
return lastSegment.replace(/\.git$/, '').trim();
|
|
21
|
+
}
|
|
22
|
+
function getRepoNameFromGitConfig(cwd) {
|
|
23
|
+
const configPath = path.join(cwd, '.git', 'config');
|
|
24
|
+
if (!fs.existsSync(configPath)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
29
|
+
const originSectionMatch = raw.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
|
|
30
|
+
if (!originSectionMatch) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const urlMatch = originSectionMatch[1].match(/^\s*url\s*=\s*(.+)$/m);
|
|
34
|
+
if (!urlMatch) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const repoName = extractRepoNameFromOriginUrl(urlMatch[1]);
|
|
38
|
+
return repoName || null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getGitRepoRoot(cwd) {
|
|
45
|
+
try {
|
|
46
|
+
const output = execSync('git rev-parse --show-toplevel', {
|
|
47
|
+
cwd,
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
50
|
+
});
|
|
51
|
+
const repoRoot = output.trim();
|
|
52
|
+
return repoRoot || null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
13
58
|
async function getGitInfo(cwd) {
|
|
14
|
-
const
|
|
59
|
+
const repoRoot = getGitRepoRoot(cwd) ?? cwd;
|
|
60
|
+
const repoNameFromGitConfig = getRepoNameFromGitConfig(repoRoot);
|
|
61
|
+
if (repoNameFromGitConfig) {
|
|
62
|
+
return { repo_name: repoNameFromGitConfig, repo_root: repoRoot };
|
|
63
|
+
}
|
|
64
|
+
const configPath = path.join(repoRoot, '.gitlab', 'config.json');
|
|
15
65
|
if (!fs.existsSync(configPath)) {
|
|
16
66
|
return null;
|
|
17
67
|
}
|
|
@@ -22,12 +72,25 @@ async function getGitInfo(cwd) {
|
|
|
22
72
|
if (!repoName) {
|
|
23
73
|
return null;
|
|
24
74
|
}
|
|
25
|
-
return { repo_name: repoName };
|
|
75
|
+
return { repo_name: repoName, repo_root: repoRoot };
|
|
26
76
|
}
|
|
27
77
|
catch {
|
|
28
78
|
return null;
|
|
29
79
|
}
|
|
30
80
|
}
|
|
81
|
+
function toRepoRelativePath(filePath, repoRoot) {
|
|
82
|
+
if (!repoRoot) {
|
|
83
|
+
return filePath;
|
|
84
|
+
}
|
|
85
|
+
const relativePath = path.relative(repoRoot, filePath);
|
|
86
|
+
if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
87
|
+
return filePath;
|
|
88
|
+
}
|
|
89
|
+
return relativePath.replace(/\\/g, '/');
|
|
90
|
+
}
|
|
91
|
+
function getKafkaFilePath(filePath, gitInfo) {
|
|
92
|
+
return toRepoRelativePath(filePath, gitInfo?.repo_root);
|
|
93
|
+
}
|
|
31
94
|
function getAddedLines(oldStr, newStr) {
|
|
32
95
|
const parts = diffLines(oldStr, newStr);
|
|
33
96
|
let lineCount = 0;
|
|
@@ -66,6 +129,54 @@ function countDiff(oldStr, newStr) {
|
|
|
66
129
|
function getFailedDir() {
|
|
67
130
|
return path.join(os.homedir(), '.mcp-tracker', 'failed');
|
|
68
131
|
}
|
|
132
|
+
function getSnapshotsDir() {
|
|
133
|
+
return path.join(os.homedir(), '.mcp-tracker', 'snapshots');
|
|
134
|
+
}
|
|
135
|
+
function ensureSnapshotsDirExists() {
|
|
136
|
+
const dir = getSnapshotsDir();
|
|
137
|
+
if (!fs.existsSync(dir)) {
|
|
138
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
return dir;
|
|
141
|
+
}
|
|
142
|
+
function getSnapshotPath(snapshotId) {
|
|
143
|
+
return path.join(getSnapshotsDir(), `${snapshotId}.json`);
|
|
144
|
+
}
|
|
145
|
+
function cleanupSnapshotsOnStartup() {
|
|
146
|
+
const dir = ensureSnapshotsDirExists();
|
|
147
|
+
const entries = fs.readdirSync(dir);
|
|
148
|
+
let deleted = 0;
|
|
149
|
+
for (const name of entries) {
|
|
150
|
+
const filePath = path.join(dir, name);
|
|
151
|
+
const stat = fs.statSync(filePath);
|
|
152
|
+
if (!stat.isFile()) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
fs.unlinkSync(filePath);
|
|
156
|
+
deleted += 1;
|
|
157
|
+
}
|
|
158
|
+
return { status: 'cleaned', deleted_count: deleted };
|
|
159
|
+
}
|
|
160
|
+
function saveSnapshot(snapshot) {
|
|
161
|
+
const dir = ensureSnapshotsDirExists();
|
|
162
|
+
const filePath = path.join(dir, `${snapshot.snapshot_id}.json`);
|
|
163
|
+
fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf8');
|
|
164
|
+
return filePath;
|
|
165
|
+
}
|
|
166
|
+
function loadSnapshot(snapshotId) {
|
|
167
|
+
const filePath = getSnapshotPath(snapshotId);
|
|
168
|
+
if (!fs.existsSync(filePath)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
172
|
+
return JSON.parse(raw);
|
|
173
|
+
}
|
|
174
|
+
function deleteSnapshot(snapshotId) {
|
|
175
|
+
const filePath = getSnapshotPath(snapshotId);
|
|
176
|
+
if (fs.existsSync(filePath)) {
|
|
177
|
+
fs.unlinkSync(filePath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
69
180
|
function cleanupFailed(maxAgeHours = 168) {
|
|
70
181
|
const dir = getFailedDir();
|
|
71
182
|
if (!fs.existsSync(dir)) {
|
|
@@ -133,16 +244,68 @@ async function replayFailed(maxCount, config) {
|
|
|
133
244
|
}
|
|
134
245
|
return { status: 'completed', replayed, failed };
|
|
135
246
|
}
|
|
247
|
+
function validateFilePath(filePath) {
|
|
248
|
+
const normalizedPath = filePath.trim();
|
|
249
|
+
if (!normalizedPath) {
|
|
250
|
+
throw new Error('file_path is required');
|
|
251
|
+
}
|
|
252
|
+
return normalizedPath;
|
|
253
|
+
}
|
|
254
|
+
function readFileContent(filePath) {
|
|
255
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
256
|
+
}
|
|
257
|
+
function getFailedEventPath(eventId) {
|
|
258
|
+
return path.join(getFailedDir(), `${eventId}.json`);
|
|
259
|
+
}
|
|
260
|
+
function handleBeforeEdit(input) {
|
|
261
|
+
const filePath = validateFilePath(input.file_path);
|
|
262
|
+
const content = readFileContent(filePath);
|
|
263
|
+
const snapshotId = randomUUID();
|
|
264
|
+
saveSnapshot({
|
|
265
|
+
snapshot_id: snapshotId,
|
|
266
|
+
file_path: filePath,
|
|
267
|
+
created_at: new Date().toISOString(),
|
|
268
|
+
content
|
|
269
|
+
});
|
|
270
|
+
return JSON.stringify({
|
|
271
|
+
status: 'snapshotted',
|
|
272
|
+
snapshot_id: snapshotId,
|
|
273
|
+
file_path: filePath
|
|
274
|
+
});
|
|
275
|
+
}
|
|
136
276
|
async function handleAfterEdit(input, config) {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
277
|
+
const filePath = validateFilePath(input.file_path);
|
|
278
|
+
const snapshotId = input.snapshot_id?.trim();
|
|
279
|
+
if (!snapshotId) {
|
|
280
|
+
throw new Error('snapshot_id is required');
|
|
281
|
+
}
|
|
282
|
+
const snapshot = loadSnapshot(snapshotId);
|
|
283
|
+
if (!snapshot) {
|
|
284
|
+
return JSON.stringify({
|
|
285
|
+
status: 'snapshot_not_found',
|
|
286
|
+
snapshot_id: snapshotId,
|
|
287
|
+
file_path: filePath
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (snapshot.file_path !== filePath) {
|
|
291
|
+
return JSON.stringify({
|
|
292
|
+
status: 'snapshot_file_path_mismatch',
|
|
293
|
+
snapshot_id: snapshotId,
|
|
294
|
+
file_path: filePath,
|
|
295
|
+
snapshot_file_path: snapshot.file_path
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const oldStr = snapshot.content;
|
|
299
|
+
const newStr = readFileContent(filePath);
|
|
300
|
+
const addedLines = getAddedLines(oldStr, newStr);
|
|
301
|
+
const diffStats = countDiff(oldStr, newStr);
|
|
139
302
|
const gitInfo = await getGitInfo(process.cwd());
|
|
140
303
|
const event = {
|
|
141
304
|
event_id: randomUUID(),
|
|
142
305
|
timestamp: new Date().toISOString(),
|
|
143
306
|
worker_id: config.worker_id,
|
|
144
|
-
git: gitInfo,
|
|
145
|
-
file: { path:
|
|
307
|
+
git: gitInfo ? { repo_name: gitInfo.repo_name } : null,
|
|
308
|
+
file: { path: getKafkaFilePath(filePath, gitInfo), operation: 'edit' },
|
|
146
309
|
changes: {
|
|
147
310
|
added_content: addedLines,
|
|
148
311
|
added: diffStats.added,
|
|
@@ -150,6 +313,16 @@ async function handleAfterEdit(input, config) {
|
|
|
150
313
|
}
|
|
151
314
|
};
|
|
152
315
|
await publishToKafka(config, event);
|
|
316
|
+
if (fs.existsSync(getFailedEventPath(event.event_id))) {
|
|
317
|
+
return JSON.stringify({
|
|
318
|
+
status: 'queued_for_retry',
|
|
319
|
+
event_id: event.event_id,
|
|
320
|
+
snapshot_id: snapshotId,
|
|
321
|
+
lines_changed: diffStats.added + diffStats.removed,
|
|
322
|
+
added_content: addedLines
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
deleteSnapshot(snapshotId);
|
|
153
326
|
return JSON.stringify({
|
|
154
327
|
status: 'recorded',
|
|
155
328
|
event_id: event.event_id,
|
|
@@ -167,17 +340,27 @@ async function handleReplayFailed(input, config) {
|
|
|
167
340
|
const result = await replayFailed(maxCount, config);
|
|
168
341
|
return JSON.stringify({ status: result.status, replayed: result.replayed, failed: result.failed });
|
|
169
342
|
}
|
|
343
|
+
const BEFORE_EDIT_TOOL_SCHEMA = {
|
|
344
|
+
name: 'before_edit',
|
|
345
|
+
description: 'Save a server-side snapshot before file edits.',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {
|
|
349
|
+
file_path: { type: 'string' }
|
|
350
|
+
},
|
|
351
|
+
required: ['file_path']
|
|
352
|
+
}
|
|
353
|
+
};
|
|
170
354
|
const AFTER_EDIT_TOOL_SCHEMA = {
|
|
171
355
|
name: 'after_edit',
|
|
172
|
-
description: 'Record code changes after file edits.',
|
|
356
|
+
description: 'Record code changes after file edits using a saved snapshot.',
|
|
173
357
|
inputSchema: {
|
|
174
358
|
type: 'object',
|
|
175
359
|
properties: {
|
|
176
360
|
file_path: { type: 'string' },
|
|
177
|
-
|
|
178
|
-
new_string: { type: 'string' }
|
|
361
|
+
snapshot_id: { type: 'string' }
|
|
179
362
|
},
|
|
180
|
-
required: ['file_path', '
|
|
363
|
+
required: ['file_path', 'snapshot_id']
|
|
181
364
|
}
|
|
182
365
|
};
|
|
183
366
|
const CLEANUP_FAILED_TOOL_SCHEMA = {
|
|
@@ -202,6 +385,7 @@ const REPLAY_FAILED_TOOL_SCHEMA = {
|
|
|
202
385
|
};
|
|
203
386
|
async function main() {
|
|
204
387
|
const config = loadConfig();
|
|
388
|
+
cleanupSnapshotsOnStartup();
|
|
205
389
|
await initProducer(config);
|
|
206
390
|
const server = new Server({
|
|
207
391
|
name: 'code-change-tracker',
|
|
@@ -212,11 +396,15 @@ async function main() {
|
|
|
212
396
|
}
|
|
213
397
|
});
|
|
214
398
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
215
|
-
tools: [AFTER_EDIT_TOOL_SCHEMA, CLEANUP_FAILED_TOOL_SCHEMA, REPLAY_FAILED_TOOL_SCHEMA]
|
|
399
|
+
tools: [BEFORE_EDIT_TOOL_SCHEMA, AFTER_EDIT_TOOL_SCHEMA, CLEANUP_FAILED_TOOL_SCHEMA, REPLAY_FAILED_TOOL_SCHEMA]
|
|
216
400
|
}));
|
|
217
401
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
218
402
|
const name = request.params.name;
|
|
219
403
|
const input = request.params.arguments ?? {};
|
|
404
|
+
if (name === 'before_edit') {
|
|
405
|
+
const result = handleBeforeEdit(input);
|
|
406
|
+
return { content: [{ type: 'text', text: result }] };
|
|
407
|
+
}
|
|
220
408
|
if (name === 'after_edit') {
|
|
221
409
|
const result = await handleAfterEdit(input, config);
|
|
222
410
|
return { content: [{ type: 'text', text: result }] };
|