mcp-code-context 2.1.3 → 2.3.1

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.
Files changed (128) hide show
  1. package/.io/RULES.md +12 -0
  2. package/CHANGELOG.md +118 -58
  3. package/CONTRIBUTING.md +243 -0
  4. package/README.md +58 -7
  5. package/RELEASE_NOTES_v2.2.0.md +286 -0
  6. package/RELEASE_NOTES_v2.3.0.md +228 -0
  7. package/RELEASE_NOTES_v2.3.1.md +110 -0
  8. package/dist/src/ast/writers/safeRename.d.ts +14 -0
  9. package/dist/src/ast/writers/safeRename.js +258 -0
  10. package/dist/src/ast/writers/safeRename.js.map +1 -0
  11. package/dist/src/ast/writers/symbolWriter.d.ts +2 -0
  12. package/dist/src/ast/writers/symbolWriter.js +9 -14
  13. package/dist/src/ast/writers/symbolWriter.js.map +1 -1
  14. package/dist/src/cache/astCache.d.ts +33 -0
  15. package/dist/src/cache/astCache.js +113 -0
  16. package/dist/src/cache/astCache.js.map +1 -0
  17. package/dist/src/handlers/readHandlers.d.ts +70 -0
  18. package/dist/src/handlers/readHandlers.js +431 -0
  19. package/dist/src/handlers/readHandlers.js.map +1 -0
  20. package/dist/src/handlers/utilHandlers.d.ts +31 -0
  21. package/dist/src/handlers/utilHandlers.js +69 -0
  22. package/dist/src/handlers/utilHandlers.js.map +1 -0
  23. package/dist/src/handlers/writeHandlers.d.ts +57 -0
  24. package/dist/src/handlers/writeHandlers.js +475 -0
  25. package/dist/src/handlers/writeHandlers.js.map +1 -0
  26. package/dist/src/index.d.ts +6 -107
  27. package/dist/src/index.js +147 -965
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/tools/readFileLines.d.ts +26 -0
  30. package/dist/src/tools/readFileLines.js +77 -0
  31. package/dist/src/tools/readFileLines.js.map +1 -0
  32. package/dist/src/tools/searchCodePattern.d.ts +32 -0
  33. package/dist/src/tools/searchCodePattern.js +80 -0
  34. package/dist/src/tools/searchCodePattern.js.map +1 -0
  35. package/dist/src/utils/arrayUtils.d.ts +12 -0
  36. package/dist/src/utils/arrayUtils.js +27 -0
  37. package/dist/src/utils/arrayUtils.js.map +1 -0
  38. package/dist/src/utils/backupManager.d.ts +13 -6
  39. package/dist/src/utils/backupManager.js +73 -8
  40. package/dist/src/utils/backupManager.js.map +1 -1
  41. package/dist/src/utils/constants.d.ts +46 -0
  42. package/dist/src/utils/constants.js +112 -0
  43. package/dist/src/utils/constants.js.map +1 -0
  44. package/dist/src/utils/ignoreManager.d.ts +8 -0
  45. package/dist/src/utils/ignoreManager.js +49 -0
  46. package/dist/src/utils/ignoreManager.js.map +1 -1
  47. package/dist/src/utils/normalization.d.ts +49 -0
  48. package/dist/src/utils/normalization.js +173 -0
  49. package/dist/src/utils/normalization.js.map +1 -0
  50. package/dist/src/utils/projectRoot.d.ts +17 -0
  51. package/dist/src/utils/projectRoot.js +42 -0
  52. package/dist/src/utils/projectRoot.js.map +1 -0
  53. package/dist/src/utils/secureValidation.d.ts +15 -0
  54. package/dist/src/utils/secureValidation.js +41 -0
  55. package/dist/src/utils/secureValidation.js.map +1 -0
  56. package/dist/src/utils/transactionManager.d.ts +45 -0
  57. package/dist/src/utils/transactionManager.js +102 -0
  58. package/dist/src/utils/transactionManager.js.map +1 -0
  59. package/dist/src/utils/validation.d.ts +87 -0
  60. package/dist/src/utils/validation.js +231 -0
  61. package/dist/src/utils/validation.js.map +1 -0
  62. package/docs/ARCHITECTURE.md +300 -0
  63. package/docs/architecture/ADR-001-modular-handlers.md +119 -0
  64. package/llms.txt +62 -7
  65. package/package.json +5 -5
  66. package/AGENT_INSTRUCTIONS.md +0 -135
  67. package/DEPLOYMENT_INSTRUCTIONS.md +0 -110
  68. package/dist/src/tests/test-confirmation-flow.d.ts +0 -1
  69. package/dist/src/tests/test-confirmation-flow.js +0 -43
  70. package/dist/src/tests/test-confirmation-flow.js.map +0 -1
  71. package/dist/tests/debug-replace.d.ts +0 -1
  72. package/dist/tests/debug-replace.js +0 -17
  73. package/dist/tests/debug-replace.js.map +0 -1
  74. package/dist/tests/test-confirmation-flow.d.ts +0 -1
  75. package/dist/tests/test-confirmation-flow.js +0 -43
  76. package/dist/tests/test-confirmation-flow.js.map +0 -1
  77. package/dist/tests/test-dart.d.ts +0 -1
  78. package/dist/tests/test-dart.js +0 -242
  79. package/dist/tests/test-dart.js.map +0 -1
  80. package/dist/tests/test-php-ast.d.ts +0 -1
  81. package/dist/tests/test-php-ast.js +0 -152
  82. package/dist/tests/test-php-ast.js.map +0 -1
  83. package/dist/tests/test-php.d.ts +0 -1
  84. package/dist/tests/test-php.js +0 -234
  85. package/dist/tests/test-php.js.map +0 -1
  86. package/dist/tests/test-smoke.d.ts +0 -5
  87. package/dist/tests/test-smoke.js +0 -83
  88. package/dist/tests/test-smoke.js.map +0 -1
  89. package/dist/tests/writers/test-crlf-handling.d.ts +0 -4
  90. package/dist/tests/writers/test-crlf-handling.js +0 -73
  91. package/dist/tests/writers/test-crlf-handling.js.map +0 -1
  92. package/dist/tests/writers/test-write-smoke.d.ts +0 -9
  93. package/dist/tests/writers/test-write-smoke.js +0 -346
  94. package/dist/tests/writers/test-write-smoke.js.map +0 -1
  95. package/src/ast/dartCompressor.ts +0 -1013
  96. package/src/ast/phpCompressor.ts +0 -814
  97. package/src/ast/semanticCompressor.ts +0 -808
  98. package/src/ast/writers/dartWriter.ts +0 -648
  99. package/src/ast/writers/phpWriter.ts +0 -508
  100. package/src/ast/writers/pyWriter.ts +0 -446
  101. package/src/ast/writers/symbolWriter.ts +0 -277
  102. package/src/ast/writers/tsWriter.ts +0 -418
  103. package/src/index.ts +0 -1553
  104. package/src/utils/backupManager.ts +0 -66
  105. package/src/utils/confirmationCache.ts +0 -61
  106. package/src/utils/diffEngine.ts +0 -223
  107. package/src/utils/fuzzyMatch.ts +0 -86
  108. package/src/utils/ignoreManager.ts +0 -144
  109. package/tests/test-confirmation-flow.ts +0 -49
  110. package/tests/test-dart.d.ts +0 -1
  111. package/tests/test-dart.js +0 -209
  112. package/tests/test-dart.js.map +0 -1
  113. package/tests/test-dart.ts +0 -265
  114. package/tests/test-php-ast.d.ts +0 -1
  115. package/tests/test-php-ast.js +0 -152
  116. package/tests/test-php-ast.js.map +0 -1
  117. package/tests/test-php-ast.ts +0 -155
  118. package/tests/test-php.d.ts +0 -1
  119. package/tests/test-php.js +0 -234
  120. package/tests/test-php.js.map +0 -1
  121. package/tests/test-php.ts +0 -261
  122. package/tests/test-smoke.d.ts +0 -5
  123. package/tests/test-smoke.js +0 -83
  124. package/tests/test-smoke.js.map +0 -1
  125. package/tests/test-smoke.ts +0 -93
  126. package/tests/writers/test-crlf-handling.ts +0 -100
  127. package/tests/writers/test-write-smoke.ts +0 -568
  128. package/tsconfig.json +0 -19
@@ -1,66 +0,0 @@
1
- /**
2
- * backupManager.ts — File Backup & Restore Utility
3
- *
4
- * Provides opt-in backup functionality for surgical write operations.
5
- * Creates `.bak` files in the same directory as the source file.
6
- *
7
- * Usage:
8
- * - Before modifying: createBackup(filePath) → returns backup path
9
- * - On failure: restoreBackup(filePath) → restores from .bak
10
- * - On success: cleanBackup(filePath) → removes .bak
11
- */
12
-
13
- import * as fs from "node:fs";
14
- import * as path from "node:path";
15
-
16
- /**
17
- * Create a backup copy of a file before modifying it.
18
- * Keeps a rolling window of the last 5 backups per file in a hidden directory.
19
- */
20
- export function createBackup(filePath: string): string {
21
- if (!fs.existsSync(filePath)) return "";
22
-
23
- const parsed = path.parse(filePath);
24
- const backupDir = path.join(parsed.dir, ".mcp-backups");
25
-
26
- if (!fs.existsSync(backupDir)) {
27
- fs.mkdirSync(backupDir, { recursive: true });
28
- }
29
-
30
- // Push existing backups down the stack (max 5)
31
- for (let i = 4; i >= 1; i--) {
32
- const oldB = path.join(backupDir, `${parsed.base}.${i}.backup`);
33
- const newB = path.join(backupDir, `${parsed.base}.${i + 1}.backup`);
34
- if (fs.existsSync(oldB)) {
35
- if (i === 4 && fs.existsSync(newB)) {
36
- fs.unlinkSync(newB); // Drop the 5th one to make room
37
- }
38
- fs.renameSync(oldB, newB);
39
- }
40
- }
41
-
42
- const backupPath = path.join(backupDir, `${parsed.base}.1.backup`);
43
- fs.copyFileSync(filePath, backupPath);
44
- return backupPath;
45
- }
46
-
47
- /**
48
- * Restore a file from its `.bak` backup.
49
- */
50
- export function restoreBackup(filePath: string, steps = 1): boolean {
51
- const parsed = path.parse(filePath);
52
- const backupDir = path.join(parsed.dir, ".mcp-backups");
53
- const targetBackup = path.join(backupDir, `${parsed.base}.${steps}.backup`);
54
-
55
- if (!fs.existsSync(targetBackup)) return false;
56
-
57
- fs.copyFileSync(targetBackup, filePath);
58
- return true;
59
- }
60
-
61
- /**
62
- * Clean legacy backups.
63
- */
64
- export function cleanBackup(filePath: string): void {
65
- // No-op. We keep the rolling 5 versions instead of deleting.
66
- }
@@ -1,61 +0,0 @@
1
- import crypto from "node:crypto";
2
-
3
- export interface PendingOperation {
4
- token: string;
5
- operation: "write_file_surgical" | "insert_symbol" | "rename_symbol" | "remove_symbol";
6
- filePath: string;
7
- payload: any; // Contains specific args needed to re-execute or apply
8
- expiresAt: number;
9
- }
10
-
11
- const CACHE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
12
-
13
- class ConfirmationCacheManager {
14
- private cache = new Map<string, PendingOperation>();
15
-
16
- public add(
17
- operation: PendingOperation["operation"],
18
- filePath: string,
19
- payload: any
20
- ): string {
21
- const token = crypto.randomUUID();
22
-
23
- this.cache.set(token, {
24
- token,
25
- operation,
26
- filePath,
27
- payload,
28
- expiresAt: Date.now() + CACHE_TIMEOUT_MS,
29
- });
30
-
31
- // Clean up expired items lazily
32
- this.cleanup();
33
-
34
- return token;
35
- }
36
-
37
- public get(token: string): PendingOperation | undefined {
38
- this.cleanup();
39
- return this.cache.get(token);
40
- }
41
-
42
- public consume(token: string): PendingOperation | undefined {
43
- this.cleanup();
44
- const op = this.cache.get(token);
45
- if (op) {
46
- this.cache.delete(token);
47
- }
48
- return op;
49
- }
50
-
51
- private cleanup() {
52
- const now = Date.now();
53
- for (const [key, value] of this.cache.entries()) {
54
- if (now > value.expiresAt) {
55
- this.cache.delete(key);
56
- }
57
- }
58
- }
59
- }
60
-
61
- export const confirmationCache = new ConfirmationCacheManager();
@@ -1,223 +0,0 @@
1
- /**
2
- * diffEngine.ts — Unified Diff Generator
3
- *
4
- * Generates human-readable unified diffs (like `git diff`) for dry-run reports
5
- * and post-write summaries. Uses a simple Myers-style diff algorithm.
6
- * No external dependencies.
7
- */
8
-
9
- // ─── Types ──────────────────────────────────────────────────────────
10
-
11
- interface DiffHunk {
12
- oldStart: number;
13
- oldCount: number;
14
- newStart: number;
15
- newCount: number;
16
- lines: string[];
17
- }
18
-
19
- // ─── Public API ─────────────────────────────────────────────────────
20
-
21
- /**
22
- * Generate a unified diff between two versions of a single file.
23
- * Returns a string formatted like `git diff --unified=3`.
24
- */
25
- export function generateDiff(
26
- oldContent: string,
27
- newContent: string,
28
- filePath: string,
29
- contextLines = 3,
30
- ): string {
31
- const oldLines = oldContent.split("\n");
32
- const newLines = newContent.split("\n");
33
-
34
- const hunks = computeHunks(oldLines, newLines, contextLines);
35
-
36
- if (hunks.length === 0) {
37
- return `--- ${filePath}\n+++ ${filePath}\n(no changes)\n`;
38
- }
39
-
40
- const header = [
41
- `--- a/${normalizedPath(filePath)}`,
42
- `+++ b/${normalizedPath(filePath)}`,
43
- ];
44
-
45
- const hunkTexts = hunks.map((hunk) => {
46
- const hunkHeader = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`;
47
- return [hunkHeader, ...hunk.lines].join("\n");
48
- });
49
-
50
- return [...header, ...hunkTexts].join("\n") + "\n";
51
- }
52
-
53
- /**
54
- * Generate a combined diff across multiple files.
55
- * Returns a single string with all file diffs separated by blank lines.
56
- */
57
- export function generateMultiFileDiff(
58
- changes: Array<{
59
- filePath: string;
60
- oldContent: string;
61
- newContent: string;
62
- }>,
63
- ): string {
64
- const diffs: string[] = [];
65
-
66
- for (const change of changes) {
67
- const diff = generateDiff(
68
- change.oldContent,
69
- change.newContent,
70
- change.filePath,
71
- );
72
- diffs.push(diff);
73
- }
74
-
75
- return diffs.join("\n");
76
- }
77
-
78
- // ─── Diff Algorithm ─────────────────────────────────────────────────
79
-
80
- /**
81
- * Compute the Longest Common Subsequence table for two arrays of strings.
82
- */
83
- function computeLCS(a: string[], b: string[]): number[][] {
84
- const m = a.length;
85
- const n = b.length;
86
-
87
- // Use a 2D array for the LCS table
88
- const dp: number[][] = Array.from({ length: m + 1 }, () =>
89
- new Array(n + 1).fill(0),
90
- );
91
-
92
- for (let i = 1; i <= m; i++) {
93
- for (let j = 1; j <= n; j++) {
94
- if (a[i - 1] === b[j - 1]) {
95
- dp[i][j] = dp[i - 1][j - 1] + 1;
96
- } else {
97
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
98
- }
99
- }
100
- }
101
-
102
- return dp;
103
- }
104
-
105
- /**
106
- * Backtrack through the LCS table to produce a list of diff operations.
107
- */
108
- function backtrackDiff(
109
- dp: number[][],
110
- a: string[],
111
- b: string[],
112
- ): Array<{ type: "keep" | "remove" | "add"; line: string }> {
113
- const result: Array<{ type: "keep" | "remove" | "add"; line: string }> = [];
114
- let i = a.length;
115
- let j = b.length;
116
-
117
- while (i > 0 || j > 0) {
118
- if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
119
- result.unshift({ type: "keep", line: a[i - 1] });
120
- i--;
121
- j--;
122
- } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
123
- result.unshift({ type: "add", line: b[j - 1] });
124
- j--;
125
- } else if (i > 0) {
126
- result.unshift({ type: "remove", line: a[i - 1] });
127
- i--;
128
- }
129
- }
130
-
131
- return result;
132
- }
133
-
134
- /**
135
- * Group diff operations into hunks with surrounding context.
136
- */
137
- function computeHunks(
138
- oldLines: string[],
139
- newLines: string[],
140
- contextLines: number,
141
- ): DiffHunk[] {
142
- const dp = computeLCS(oldLines, newLines);
143
- const ops = backtrackDiff(dp, oldLines, newLines);
144
-
145
- // Find ranges of changes (non-"keep" operations)
146
- const changeIndices: number[] = [];
147
- for (let i = 0; i < ops.length; i++) {
148
- if (ops[i].type !== "keep") {
149
- changeIndices.push(i);
150
- }
151
- }
152
-
153
- if (changeIndices.length === 0) return [];
154
-
155
- // Group nearby changes into hunks
156
- const groups: Array<{ start: number; end: number }> = [];
157
- let groupStart = changeIndices[0];
158
- let groupEnd = changeIndices[0];
159
-
160
- for (let k = 1; k < changeIndices.length; k++) {
161
- if (changeIndices[k] - groupEnd <= contextLines * 2 + 1) {
162
- // Merge into current group
163
- groupEnd = changeIndices[k];
164
- } else {
165
- groups.push({ start: groupStart, end: groupEnd });
166
- groupStart = changeIndices[k];
167
- groupEnd = changeIndices[k];
168
- }
169
- }
170
- groups.push({ start: groupStart, end: groupEnd });
171
-
172
- // Build hunks with context
173
- const hunks: DiffHunk[] = [];
174
-
175
- for (const group of groups) {
176
- const hunkStart = Math.max(0, group.start - contextLines);
177
- const hunkEnd = Math.min(ops.length - 1, group.end + contextLines);
178
-
179
- const lines: string[] = [];
180
- let oldLineNum = 0;
181
- let newLineNum = 0;
182
-
183
- // Count lines before hunk start to get correct line numbers
184
- for (let i = 0; i < hunkStart; i++) {
185
- if (ops[i].type === "keep" || ops[i].type === "remove") oldLineNum++;
186
- if (ops[i].type === "keep" || ops[i].type === "add") newLineNum++;
187
- }
188
-
189
- const oldStart = oldLineNum + 1;
190
- const newStart = newLineNum + 1;
191
- let oldCount = 0;
192
- let newCount = 0;
193
-
194
- for (let i = hunkStart; i <= hunkEnd; i++) {
195
- const op = ops[i];
196
- switch (op.type) {
197
- case "keep":
198
- lines.push(` ${op.line}`);
199
- oldCount++;
200
- newCount++;
201
- break;
202
- case "remove":
203
- lines.push(`-${op.line}`);
204
- oldCount++;
205
- break;
206
- case "add":
207
- lines.push(`+${op.line}`);
208
- newCount++;
209
- break;
210
- }
211
- }
212
-
213
- hunks.push({ oldStart, oldCount, newStart, newCount, lines });
214
- }
215
-
216
- return hunks;
217
- }
218
-
219
- // ─── Utilities ──────────────────────────────────────────────────────
220
-
221
- function normalizedPath(filePath: string): string {
222
- return filePath.replace(/\\/g, "/");
223
- }
@@ -1,86 +0,0 @@
1
- export interface SymbolInfo {
2
- name: string;
3
- type: string;
4
- className?: string;
5
- }
6
-
7
- export function levenshteinDistance(a: string, b: string): number {
8
- const matrix = [];
9
- let i, j;
10
-
11
- if (a.length === 0) return b.length;
12
- if (b.length === 0) return a.length;
13
-
14
- for (i = 0; i <= b.length; i++) {
15
- matrix[i] = [i];
16
- }
17
- for (j = 0; j <= a.length; j++) {
18
- matrix[0][j] = j;
19
- }
20
-
21
- for (i = 1; i <= b.length; i++) {
22
- for (j = 1; j <= a.length; j++) {
23
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
24
- matrix[i][j] = matrix[i - 1][j - 1];
25
- } else {
26
- matrix[i][j] = Math.min(
27
- matrix[i - 1][j - 1] + 1,
28
- Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1)
29
- );
30
- }
31
- }
32
- }
33
-
34
- return matrix[b.length][a.length];
35
- }
36
-
37
- export function findClosestSymbols(target: string, symbols: SymbolInfo[], limit = 5): SymbolInfo[] {
38
- return symbols
39
- .map((sym) => ({
40
- ...sym,
41
- distance: levenshteinDistance(target.toLowerCase(), sym.name.toLowerCase()),
42
- }))
43
- .sort((a, b) => a.distance - b.distance)
44
- .slice(0, limit);
45
- }
46
-
47
- // A simple regex-based extractor to get available symbols when the AST parsing fails
48
- export function extractFallbackSymbols(content: string): SymbolInfo[] {
49
- const symbols: SymbolInfo[] = [];
50
- const lines = content.split('\\n');
51
-
52
- let currentClass: string | undefined = undefined;
53
-
54
- for (let i = 0; i < lines.length; i++) {
55
- const line = lines[i].trim();
56
-
57
- // Check for class definitions (multi-language approx)
58
- const classMatch = line.match(/^\s*(?:export\s+|abstract\s+|final\s+)?class\s+([_a-zA-Z0-9]+)/);
59
- if (classMatch) {
60
- currentClass = classMatch[1];
61
- symbols.push({ name: currentClass, type: 'class' });
62
- continue;
63
- }
64
-
65
- // Check for function / method definitions
66
- // JS/TS: function foo() | foo() { | async foo()
67
- // Python: def foo()
68
- // PHP: function foo()
69
- // Dart: type foo()
70
- const funcMatch = line.match(/^\s*(?:export\s+|async\s+|public\s+|private\s+|protected\s+|static\s+|def\s+|function\s+)*([_a-zA-Z0-9]+)\s*\(/);
71
- if (funcMatch && !['if', 'else', 'for', 'while', 'switch', 'catch'].includes(funcMatch[1])) {
72
- symbols.push({
73
- name: funcMatch[1],
74
- type: currentClass ? 'method' : 'function',
75
- className: currentClass,
76
- });
77
- }
78
-
79
- // Basic assumption: closing brace at start of line might end a class
80
- if (line === '}') {
81
- currentClass = undefined;
82
- }
83
- }
84
-
85
- return symbols;
86
- }
@@ -1,144 +0,0 @@
1
- /**
2
- * IgnoreManager — Reads .gitignore / .repomixignore and walks directories
3
- * filtering out ignored paths. Always excludes common heavy directories.
4
- */
5
-
6
- import * as fs from "node:fs";
7
- import * as path from "node:path";
8
- import { createRequire } from "node:module";
9
- import type { Ignore } from "ignore";
10
-
11
- // CJS interop: 'ignore' uses module.exports which doesn't resolve
12
- // as a callable default export under Node16 module resolution.
13
- const require = createRequire(import.meta.url);
14
- const ignore: (opts?: Record<string, unknown>) => Ignore = require("ignore");
15
-
16
- /** Directories and patterns that are ALWAYS excluded regardless of .gitignore */
17
- const ALWAYS_IGNORE: string[] = [
18
- "node_modules",
19
- ".git",
20
- "dist",
21
- "build",
22
- "vendor",
23
- ".next",
24
- ".nuxt",
25
- ".output",
26
- "__pycache__",
27
- ".pytest_cache",
28
- "coverage",
29
- ".nyc_output",
30
- ".turbo",
31
- ".cache",
32
- ".parcel-cache",
33
- ".venv",
34
- "venv",
35
- "env",
36
- ".env.local",
37
- ".tsbuildinfo",
38
- "*.lock",
39
- "package-lock.json",
40
- "yarn.lock",
41
- "pnpm-lock.yaml",
42
- "bun.lockb",
43
- "*.min.js",
44
- "*.min.css",
45
- "*.map",
46
- "*.chunk.js",
47
- "*.bundle.js",
48
- ];
49
-
50
- /** Max file size to read (512 KB). Files larger than this are skipped. */
51
- const MAX_FILE_SIZE_BYTES = 512 * 1024;
52
-
53
- export class IgnoreManager {
54
- private ig: Ignore;
55
- private rootDir: string;
56
-
57
- constructor(rootDir: string) {
58
- this.rootDir = path.resolve(rootDir);
59
- this.ig = ignore();
60
-
61
- // Load hardcoded ignores first
62
- this.ig.add(ALWAYS_IGNORE);
63
-
64
- // Load .gitignore
65
- this.loadIgnoreFile(path.join(this.rootDir, ".gitignore"));
66
-
67
- // Load .repomixignore (Repomix-style overrides)
68
- this.loadIgnoreFile(path.join(this.rootDir, ".repomixignore"));
69
- }
70
-
71
- /**
72
- * Reads an ignore-pattern file and adds its rules to the filter.
73
- * Silently skips if the file doesn't exist or is unreadable.
74
- */
75
- private loadIgnoreFile(filePath: string): void {
76
- try {
77
- if (!fs.existsSync(filePath)) return;
78
-
79
- const content = fs.readFileSync(filePath, "utf-8");
80
- const patterns = content
81
- .split("\n")
82
- .map((line) => line.trim())
83
- .filter((line) => line.length > 0 && !line.startsWith("#"));
84
-
85
- if (patterns.length > 0) {
86
- this.ig.add(patterns);
87
- }
88
- } catch {
89
- // File unreadable — skip silently
90
- }
91
- }
92
-
93
- /** Check if a relative path should be ignored. */
94
- public isIgnored(relativePath: string): boolean {
95
- if (!relativePath) return false;
96
- return this.ig.ignores(relativePath);
97
- }
98
-
99
- /**
100
- * Recursively walk a directory, returning absolute paths to all
101
- * non-ignored files.
102
- */
103
- public walkDirectory(dir?: string): string[] {
104
- const baseDir = dir ? path.resolve(dir) : this.rootDir;
105
- const files: string[] = [];
106
- this.walk(baseDir, files);
107
- return files;
108
- }
109
-
110
- private walk(currentDir: string, collected: string[]): void {
111
- let entries: fs.Dirent[];
112
- try {
113
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
114
- } catch {
115
- return; // Permission denied, broken symlink, etc.
116
- }
117
-
118
- for (const entry of entries) {
119
- const fullPath = path.join(currentDir, entry.name);
120
- const relativePath = path
121
- .relative(this.rootDir, fullPath)
122
- .replace(/\\/g, "/");
123
-
124
- // Check ignore rules
125
- if (this.isIgnored(relativePath)) continue;
126
-
127
- if (entry.isDirectory()) {
128
- // Also test with trailing slash for directory-level patterns
129
- if (this.isIgnored(relativePath + "/")) continue;
130
- this.walk(fullPath, collected);
131
- } else if (entry.isFile()) {
132
- // Skip files that are too large
133
- try {
134
- const stat = fs.statSync(fullPath);
135
- if (stat.size > MAX_FILE_SIZE_BYTES) continue;
136
- } catch {
137
- continue;
138
- }
139
- collected.push(fullPath);
140
- }
141
- // Symlinks and other entries are silently skipped
142
- }
143
- }
144
- }
@@ -1,49 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import { handleWriteFileSurgical, handleRollbackFile } from '../src/index.js';
4
-
5
- async function testWorkflow() {
6
- const testFile = path.resolve('test-artifact.ts');
7
- const initialContent = 'export class Old { build() { return 1; } }';
8
- fs.writeFileSync(testFile, initialContent, 'utf-8');
9
-
10
- console.log('--- Step 1: Preview (Dry-Run) ---');
11
- const previewArgs = {
12
- filePath: testFile,
13
- symbolName: 'build',
14
- newContent: ' build() { return "new value"; }',
15
- className: 'Old'
16
- };
17
-
18
- const p1 = await handleWriteFileSurgical(previewArgs);
19
- const p1Text = p1.content[0].text;
20
- console.log('Preview Result:', p1Text.split('\n')[0]);
21
-
22
- const tokenMatch = p1Text.match(/confirmationToken: "([^"]+)"/);
23
- if (!tokenMatch) throw new Error('No token found');
24
- const token = tokenMatch[1];
25
- console.log('Token found:', token);
26
-
27
- console.log('\n--- Step 2: Confirm ---');
28
- const confirmArgs = {
29
- ...previewArgs,
30
- confirmationToken: token,
31
- confirm: true
32
- };
33
- const p2 = await handleWriteFileSurgical(confirmArgs);
34
- console.log('Confirm Result:', p2.content[0].text.split('\n')[0]);
35
- console.log('File Content:', fs.readFileSync(testFile, 'utf-8'));
36
-
37
- console.log('\n--- Step 3: Rollback ---');
38
- await handleRollbackFile({ filePath: testFile, steps: 1 });
39
- console.log('After Rollback:', fs.readFileSync(testFile, 'utf-8'));
40
-
41
- // Clean up
42
- fs.unlinkSync(testFile);
43
- const backupDir = path.join(path.dirname(testFile), '.mcp-backups');
44
- if (fs.existsSync(backupDir)) {
45
- fs.rmSync(backupDir, { recursive: true, force: true });
46
- }
47
- }
48
-
49
- testWorkflow().catch(console.error);
@@ -1 +0,0 @@
1
- export {};