ronds-mcp-tracker 0.1.6 → 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 +167 -13
- 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';
|
|
@@ -40,12 +41,27 @@ function getRepoNameFromGitConfig(cwd) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
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
|
+
}
|
|
43
58
|
async function getGitInfo(cwd) {
|
|
44
|
-
const
|
|
59
|
+
const repoRoot = getGitRepoRoot(cwd) ?? cwd;
|
|
60
|
+
const repoNameFromGitConfig = getRepoNameFromGitConfig(repoRoot);
|
|
45
61
|
if (repoNameFromGitConfig) {
|
|
46
|
-
return { repo_name: repoNameFromGitConfig };
|
|
62
|
+
return { repo_name: repoNameFromGitConfig, repo_root: repoRoot };
|
|
47
63
|
}
|
|
48
|
-
const configPath = path.join(
|
|
64
|
+
const configPath = path.join(repoRoot, '.gitlab', 'config.json');
|
|
49
65
|
if (!fs.existsSync(configPath)) {
|
|
50
66
|
return null;
|
|
51
67
|
}
|
|
@@ -56,12 +72,25 @@ async function getGitInfo(cwd) {
|
|
|
56
72
|
if (!repoName) {
|
|
57
73
|
return null;
|
|
58
74
|
}
|
|
59
|
-
return { repo_name: repoName };
|
|
75
|
+
return { repo_name: repoName, repo_root: repoRoot };
|
|
60
76
|
}
|
|
61
77
|
catch {
|
|
62
78
|
return null;
|
|
63
79
|
}
|
|
64
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
|
+
}
|
|
65
94
|
function getAddedLines(oldStr, newStr) {
|
|
66
95
|
const parts = diffLines(oldStr, newStr);
|
|
67
96
|
let lineCount = 0;
|
|
@@ -100,6 +129,54 @@ function countDiff(oldStr, newStr) {
|
|
|
100
129
|
function getFailedDir() {
|
|
101
130
|
return path.join(os.homedir(), '.mcp-tracker', 'failed');
|
|
102
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
|
+
}
|
|
103
180
|
function cleanupFailed(maxAgeHours = 168) {
|
|
104
181
|
const dir = getFailedDir();
|
|
105
182
|
if (!fs.existsSync(dir)) {
|
|
@@ -167,16 +244,68 @@ async function replayFailed(maxCount, config) {
|
|
|
167
244
|
}
|
|
168
245
|
return { status: 'completed', replayed, failed };
|
|
169
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
|
+
}
|
|
170
276
|
async function handleAfterEdit(input, config) {
|
|
171
|
-
const
|
|
172
|
-
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);
|
|
173
302
|
const gitInfo = await getGitInfo(process.cwd());
|
|
174
303
|
const event = {
|
|
175
304
|
event_id: randomUUID(),
|
|
176
305
|
timestamp: new Date().toISOString(),
|
|
177
306
|
worker_id: config.worker_id,
|
|
178
|
-
git: gitInfo,
|
|
179
|
-
file: { path:
|
|
307
|
+
git: gitInfo ? { repo_name: gitInfo.repo_name } : null,
|
|
308
|
+
file: { path: getKafkaFilePath(filePath, gitInfo), operation: 'edit' },
|
|
180
309
|
changes: {
|
|
181
310
|
added_content: addedLines,
|
|
182
311
|
added: diffStats.added,
|
|
@@ -184,6 +313,16 @@ async function handleAfterEdit(input, config) {
|
|
|
184
313
|
}
|
|
185
314
|
};
|
|
186
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);
|
|
187
326
|
return JSON.stringify({
|
|
188
327
|
status: 'recorded',
|
|
189
328
|
event_id: event.event_id,
|
|
@@ -201,17 +340,27 @@ async function handleReplayFailed(input, config) {
|
|
|
201
340
|
const result = await replayFailed(maxCount, config);
|
|
202
341
|
return JSON.stringify({ status: result.status, replayed: result.replayed, failed: result.failed });
|
|
203
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
|
+
};
|
|
204
354
|
const AFTER_EDIT_TOOL_SCHEMA = {
|
|
205
355
|
name: 'after_edit',
|
|
206
|
-
description: 'Record code changes after file edits.',
|
|
356
|
+
description: 'Record code changes after file edits using a saved snapshot.',
|
|
207
357
|
inputSchema: {
|
|
208
358
|
type: 'object',
|
|
209
359
|
properties: {
|
|
210
360
|
file_path: { type: 'string' },
|
|
211
|
-
|
|
212
|
-
new_string: { type: 'string' }
|
|
361
|
+
snapshot_id: { type: 'string' }
|
|
213
362
|
},
|
|
214
|
-
required: ['file_path', '
|
|
363
|
+
required: ['file_path', 'snapshot_id']
|
|
215
364
|
}
|
|
216
365
|
};
|
|
217
366
|
const CLEANUP_FAILED_TOOL_SCHEMA = {
|
|
@@ -236,6 +385,7 @@ const REPLAY_FAILED_TOOL_SCHEMA = {
|
|
|
236
385
|
};
|
|
237
386
|
async function main() {
|
|
238
387
|
const config = loadConfig();
|
|
388
|
+
cleanupSnapshotsOnStartup();
|
|
239
389
|
await initProducer(config);
|
|
240
390
|
const server = new Server({
|
|
241
391
|
name: 'code-change-tracker',
|
|
@@ -246,11 +396,15 @@ async function main() {
|
|
|
246
396
|
}
|
|
247
397
|
});
|
|
248
398
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
249
|
-
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]
|
|
250
400
|
}));
|
|
251
401
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
252
402
|
const name = request.params.name;
|
|
253
403
|
const input = request.params.arguments ?? {};
|
|
404
|
+
if (name === 'before_edit') {
|
|
405
|
+
const result = handleBeforeEdit(input);
|
|
406
|
+
return { content: [{ type: 'text', text: result }] };
|
|
407
|
+
}
|
|
254
408
|
if (name === 'after_edit') {
|
|
255
409
|
const result = await handleAfterEdit(input, config);
|
|
256
410
|
return { content: [{ type: 'text', text: result }] };
|