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 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 repoNameFromGitConfig = getRepoNameFromGitConfig(cwd);
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(cwd, '.gitlab', 'config.json');
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 addedLines = getAddedLines(input.old_string, input.new_string);
172
- 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);
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: input.file_path, operation: 'edit' },
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
- old_string: { type: 'string' },
212
- new_string: { type: 'string' }
361
+ snapshot_id: { type: 'string' }
213
362
  },
214
- required: ['file_path', 'old_string', 'new_string']
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 }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronds-mcp-tracker",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ronds-mcp-tracker": "./bin/cli.js"