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 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 configPath = path.join(cwd, '.gitlab', 'config.json');
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 addedLines = getAddedLines(input.old_string, input.new_string);
138
- const diffStats = countDiff(input.old_string, input.new_string);
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: input.file_path, operation: 'edit' },
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
- old_string: { type: 'string' },
178
- new_string: { type: 'string' }
361
+ snapshot_id: { type: 'string' }
179
362
  },
180
- required: ['file_path', 'old_string', 'new_string']
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 }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronds-mcp-tracker",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ronds-mcp-tracker": "./bin/cli.js"