mcp-code-context 2.2.0 → 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.
- package/.io/RULES.md +12 -0
- package/CHANGELOG.md +88 -1
- package/README.md +27 -0
- package/RELEASE_NOTES_v2.3.0.md +228 -0
- package/RELEASE_NOTES_v2.3.1.md +110 -0
- package/dist/src/ast/writers/safeRename.d.ts +14 -0
- package/dist/src/ast/writers/safeRename.js +258 -0
- package/dist/src/ast/writers/safeRename.js.map +1 -0
- package/dist/src/ast/writers/symbolWriter.d.ts +2 -0
- package/dist/src/ast/writers/symbolWriter.js +9 -14
- package/dist/src/ast/writers/symbolWriter.js.map +1 -1
- package/dist/src/cache/astCache.d.ts +33 -0
- package/dist/src/cache/astCache.js +113 -0
- package/dist/src/cache/astCache.js.map +1 -0
- package/dist/src/handlers/readHandlers.d.ts +70 -0
- package/dist/src/handlers/readHandlers.js +431 -0
- package/dist/src/handlers/readHandlers.js.map +1 -0
- package/dist/src/handlers/utilHandlers.d.ts +31 -0
- package/dist/src/handlers/utilHandlers.js +69 -0
- package/dist/src/handlers/utilHandlers.js.map +1 -0
- package/dist/src/handlers/writeHandlers.d.ts +57 -0
- package/dist/src/handlers/writeHandlers.js +475 -0
- package/dist/src/handlers/writeHandlers.js.map +1 -0
- package/dist/src/index.d.ts +6 -143
- package/dist/src/index.js +77 -1091
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/arrayUtils.d.ts +12 -0
- package/dist/src/utils/arrayUtils.js +27 -0
- package/dist/src/utils/arrayUtils.js.map +1 -0
- package/dist/src/utils/constants.d.ts +46 -0
- package/dist/src/utils/constants.js +112 -0
- package/dist/src/utils/constants.js.map +1 -0
- package/dist/src/utils/ignoreManager.d.ts +8 -0
- package/dist/src/utils/ignoreManager.js +49 -0
- package/dist/src/utils/ignoreManager.js.map +1 -1
- package/dist/src/utils/normalization.d.ts +49 -0
- package/dist/src/utils/normalization.js +173 -0
- package/dist/src/utils/normalization.js.map +1 -0
- package/dist/src/utils/projectRoot.d.ts +12 -5
- package/dist/src/utils/projectRoot.js +22 -20
- package/dist/src/utils/projectRoot.js.map +1 -1
- package/dist/src/utils/secureValidation.d.ts +15 -0
- package/dist/src/utils/secureValidation.js +41 -0
- package/dist/src/utils/secureValidation.js.map +1 -0
- package/dist/src/utils/transactionManager.d.ts +45 -0
- package/dist/src/utils/transactionManager.js +102 -0
- package/dist/src/utils/transactionManager.js.map +1 -0
- package/dist/src/utils/validation.d.ts +87 -0
- package/dist/src/utils/validation.js +231 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/docs/ARCHITECTURE.md +300 -0
- package/docs/architecture/ADR-001-modular-handlers.md +119 -0
- package/package.json +5 -5
- package/AGENT_INSTRUCTIONS.md +0 -135
- package/DEPLOYMENT_INSTRUCTIONS.md +0 -110
- package/MEJORAS_PROPUESTAS.md +0 -324
- package/dist/src/tests/test-confirmation-flow.d.ts +0 -1
- package/dist/src/tests/test-confirmation-flow.js +0 -43
- package/dist/src/tests/test-confirmation-flow.js.map +0 -1
- package/dist/tests/debug-replace.d.ts +0 -1
- package/dist/tests/debug-replace.js +0 -17
- package/dist/tests/debug-replace.js.map +0 -1
- package/dist/tests/test-backup-system.d.ts +0 -7
- package/dist/tests/test-backup-system.js +0 -152
- package/dist/tests/test-backup-system.js.map +0 -1
- package/dist/tests/test-confirmation-flow.d.ts +0 -1
- package/dist/tests/test-confirmation-flow.js +0 -43
- package/dist/tests/test-confirmation-flow.js.map +0 -1
- package/dist/tests/test-dart.d.ts +0 -1
- package/dist/tests/test-dart.js +0 -242
- package/dist/tests/test-dart.js.map +0 -1
- package/dist/tests/test-new-tools.d.ts +0 -4
- package/dist/tests/test-new-tools.js +0 -147
- package/dist/tests/test-new-tools.js.map +0 -1
- package/dist/tests/test-php-ast.d.ts +0 -1
- package/dist/tests/test-php-ast.js +0 -152
- package/dist/tests/test-php-ast.js.map +0 -1
- package/dist/tests/test-php.d.ts +0 -1
- package/dist/tests/test-php.js +0 -234
- package/dist/tests/test-php.js.map +0 -1
- package/dist/tests/test-smoke.d.ts +0 -5
- package/dist/tests/test-smoke.js +0 -83
- package/dist/tests/test-smoke.js.map +0 -1
- package/dist/tests/writers/test-crlf-handling.d.ts +0 -4
- package/dist/tests/writers/test-crlf-handling.js +0 -73
- package/dist/tests/writers/test-crlf-handling.js.map +0 -1
- package/dist/tests/writers/test-write-smoke.d.ts +0 -9
- package/dist/tests/writers/test-write-smoke.js +0 -346
- package/dist/tests/writers/test-write-smoke.js.map +0 -1
- package/src/ast/dartCompressor.ts +0 -1013
- package/src/ast/phpCompressor.ts +0 -814
- package/src/ast/semanticCompressor.ts +0 -808
- package/src/ast/writers/dartWriter.ts +0 -648
- package/src/ast/writers/phpWriter.ts +0 -508
- package/src/ast/writers/pyWriter.ts +0 -446
- package/src/ast/writers/symbolWriter.ts +0 -277
- package/src/ast/writers/tsWriter.ts +0 -418
- package/src/index.ts +0 -1784
- package/src/tools/readFileLines.ts +0 -104
- package/src/tools/searchCodePattern.ts +0 -129
- package/src/utils/backupManager.ts +0 -141
- package/src/utils/confirmationCache.ts +0 -61
- package/src/utils/diffEngine.ts +0 -223
- package/src/utils/fuzzyMatch.ts +0 -86
- package/src/utils/ignoreManager.ts +0 -144
- package/src/utils/projectRoot.ts +0 -44
- package/tests/test-backup-system.ts +0 -174
- package/tests/test-confirmation-flow.ts +0 -49
- package/tests/test-dart.d.ts +0 -1
- package/tests/test-dart.js +0 -209
- package/tests/test-dart.js.map +0 -1
- package/tests/test-dart.ts +0 -265
- package/tests/test-new-tools.ts +0 -165
- package/tests/test-php-ast.d.ts +0 -1
- package/tests/test-php-ast.js +0 -152
- package/tests/test-php-ast.js.map +0 -1
- package/tests/test-php-ast.ts +0 -155
- package/tests/test-php.d.ts +0 -1
- package/tests/test-php.js +0 -234
- package/tests/test-php.js.map +0 -1
- package/tests/test-php.ts +0 -261
- package/tests/test-smoke.d.ts +0 -5
- package/tests/test-smoke.js +0 -83
- package/tests/test-smoke.js.map +0 -1
- package/tests/test-smoke.ts +0 -93
- package/tests/writers/test-crlf-handling.ts +0 -100
- package/tests/writers/test-write-smoke.ts +0 -568
- package/tsconfig.json +0 -19
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* read_file_lines — Read specific line ranges from a file
|
|
3
|
-
*
|
|
4
|
-
* Efficiently reads only the requested lines without loading the entire file into memory.
|
|
5
|
-
* Supports reading by exact line range or around a pattern match.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
|
|
11
|
-
export interface ReadFileLinesArgs {
|
|
12
|
-
filePath: string;
|
|
13
|
-
startLine?: number;
|
|
14
|
-
endLine?: number;
|
|
15
|
-
aroundPattern?: string;
|
|
16
|
-
contextLines?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ReadFileLinesResult {
|
|
20
|
-
success: boolean;
|
|
21
|
-
content?: string;
|
|
22
|
-
error?: string;
|
|
23
|
-
lineRange?: { start: number; end: number };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Read specific lines from a file
|
|
28
|
-
*/
|
|
29
|
-
export function readFileLines(args: ReadFileLinesArgs): ReadFileLinesResult {
|
|
30
|
-
const { filePath, startLine, endLine, aroundPattern, contextLines = 5 } = args;
|
|
31
|
-
|
|
32
|
-
const resolvedPath = path.resolve(filePath);
|
|
33
|
-
|
|
34
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
35
|
-
return {
|
|
36
|
-
success: false,
|
|
37
|
-
error: `File not found: ${resolvedPath}`,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let content: string;
|
|
42
|
-
try {
|
|
43
|
-
content = fs.readFileSync(resolvedPath, "utf-8");
|
|
44
|
-
} catch (error: unknown) {
|
|
45
|
-
return {
|
|
46
|
-
success: false,
|
|
47
|
-
error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const lines = content.split("\n");
|
|
52
|
-
const totalLines = lines.length;
|
|
53
|
-
|
|
54
|
-
// Mode 1: Exact line range
|
|
55
|
-
if (startLine !== undefined && endLine !== undefined) {
|
|
56
|
-
if (startLine < 1 || endLine < 1 || startLine > totalLines || endLine > totalLines) {
|
|
57
|
-
return {
|
|
58
|
-
success: false,
|
|
59
|
-
error: `Invalid line range: ${startLine}-${endLine}. File has ${totalLines} lines.`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (startLine > endLine) {
|
|
64
|
-
return {
|
|
65
|
-
success: false,
|
|
66
|
-
error: `startLine (${startLine}) must be <= endLine (${endLine})`,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const selectedLines = lines.slice(startLine - 1, endLine);
|
|
71
|
-
return {
|
|
72
|
-
success: true,
|
|
73
|
-
content: selectedLines.join("\n"),
|
|
74
|
-
lineRange: { start: startLine, end: endLine },
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Mode 2: Around pattern
|
|
79
|
-
if (aroundPattern) {
|
|
80
|
-
const matchIndex = lines.findIndex((line) => line.includes(aroundPattern));
|
|
81
|
-
|
|
82
|
-
if (matchIndex === -1) {
|
|
83
|
-
return {
|
|
84
|
-
success: false,
|
|
85
|
-
error: `Pattern "${aroundPattern}" not found in file`,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const start = Math.max(0, matchIndex - contextLines);
|
|
90
|
-
const end = Math.min(totalLines - 1, matchIndex + contextLines);
|
|
91
|
-
|
|
92
|
-
const selectedLines = lines.slice(start, end + 1);
|
|
93
|
-
return {
|
|
94
|
-
success: true,
|
|
95
|
-
content: selectedLines.join("\n"),
|
|
96
|
-
lineRange: { start: start + 1, end: end + 1 },
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
success: false,
|
|
102
|
-
error: "Must provide either (startLine + endLine) or aroundPattern",
|
|
103
|
-
};
|
|
104
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* search_code_pattern — Search for code patterns across files
|
|
3
|
-
*
|
|
4
|
-
* AST-aware search that finds patterns in code while optionally excluding
|
|
5
|
-
* matches in strings, comments, and other non-code contexts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import { IgnoreManager } from "../utils/ignoreManager.js";
|
|
11
|
-
|
|
12
|
-
export interface SearchCodePatternArgs {
|
|
13
|
-
rootDir: string;
|
|
14
|
-
pattern: string;
|
|
15
|
-
fileExtensions?: string[];
|
|
16
|
-
excludeDirs?: string[];
|
|
17
|
-
showContext?: boolean;
|
|
18
|
-
contextLines?: number;
|
|
19
|
-
maxResults?: number;
|
|
20
|
-
astAware?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface SearchMatch {
|
|
24
|
-
file: string;
|
|
25
|
-
lineNumber: number;
|
|
26
|
-
line: string;
|
|
27
|
-
context?: string[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface SearchCodePatternResult {
|
|
31
|
-
success: boolean;
|
|
32
|
-
matches?: SearchMatch[];
|
|
33
|
-
error?: string;
|
|
34
|
-
totalMatches?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const DEFAULT_EXTENSIONS = [
|
|
38
|
-
".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs",
|
|
39
|
-
".py", ".php", ".dart", ".java", ".go", ".rs"
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Search for a pattern across code files
|
|
44
|
-
*/
|
|
45
|
-
export function searchCodePattern(args: SearchCodePatternArgs): SearchCodePatternResult {
|
|
46
|
-
const {
|
|
47
|
-
rootDir,
|
|
48
|
-
pattern,
|
|
49
|
-
fileExtensions = DEFAULT_EXTENSIONS,
|
|
50
|
-
excludeDirs = ["node_modules", "dist", "build", ".git"],
|
|
51
|
-
showContext = true,
|
|
52
|
-
contextLines = 3,
|
|
53
|
-
maxResults = 50,
|
|
54
|
-
astAware = false,
|
|
55
|
-
} = args;
|
|
56
|
-
|
|
57
|
-
const resolvedRoot = path.resolve(rootDir);
|
|
58
|
-
|
|
59
|
-
if (!fs.existsSync(resolvedRoot)) {
|
|
60
|
-
return {
|
|
61
|
-
success: false,
|
|
62
|
-
error: `Directory not found: ${resolvedRoot}`,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const ignoreManager = new IgnoreManager(resolvedRoot);
|
|
67
|
-
const allFiles = ignoreManager.walkDirectory();
|
|
68
|
-
|
|
69
|
-
const matches: SearchMatch[] = [];
|
|
70
|
-
let totalMatches = 0;
|
|
71
|
-
|
|
72
|
-
const regex = new RegExp(pattern, "gi");
|
|
73
|
-
|
|
74
|
-
for (const file of allFiles) {
|
|
75
|
-
// Check extension
|
|
76
|
-
const ext = path.extname(file).toLowerCase();
|
|
77
|
-
if (!fileExtensions.includes(ext)) continue;
|
|
78
|
-
|
|
79
|
-
// Check excluded directories
|
|
80
|
-
const relativePath = path.relative(resolvedRoot, file);
|
|
81
|
-
if (excludeDirs.some(dir => relativePath.includes(dir))) continue;
|
|
82
|
-
|
|
83
|
-
let content: string;
|
|
84
|
-
try {
|
|
85
|
-
content = fs.readFileSync(file, "utf-8");
|
|
86
|
-
} catch {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const lines = content.split("\n");
|
|
91
|
-
|
|
92
|
-
for (let i = 0; i < lines.length; i++) {
|
|
93
|
-
const line = lines[i];
|
|
94
|
-
|
|
95
|
-
if (regex.test(line)) {
|
|
96
|
-
totalMatches++;
|
|
97
|
-
|
|
98
|
-
if (matches.length < maxResults) {
|
|
99
|
-
const match: SearchMatch = {
|
|
100
|
-
file: path.relative(resolvedRoot, file).replace(/\\/g, "/"),
|
|
101
|
-
lineNumber: i + 1,
|
|
102
|
-
line: line.trim(),
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (showContext) {
|
|
106
|
-
const start = Math.max(0, i - contextLines);
|
|
107
|
-
const end = Math.min(lines.length - 1, i + contextLines);
|
|
108
|
-
match.context = lines.slice(start, end + 1).map((l, idx) => {
|
|
109
|
-
const lineNum = start + idx + 1;
|
|
110
|
-
const marker = lineNum === i + 1 ? ">" : " ";
|
|
111
|
-
return `${marker} ${lineNum}: ${l}`;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
matches.push(match);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Reset regex lastIndex for global flag
|
|
120
|
-
regex.lastIndex = 0;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
success: true,
|
|
126
|
-
matches,
|
|
127
|
-
totalMatches,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* backupManager.ts — File Backup & Restore Utility
|
|
3
|
-
*
|
|
4
|
-
* Provides opt-in backup functionality for surgical write operations.
|
|
5
|
-
* Creates centralized `.mcp-backups/` directory at project root.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* - Before modifying: createBackup(filePath) → returns backup path
|
|
9
|
-
* - On failure: restoreBackup(filePath) → restores from backup
|
|
10
|
-
* - Cleanup: cleanAllBackups(projectRoot) → removes all backups
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import * as fs from "node:fs";
|
|
14
|
-
import * as path from "node:path";
|
|
15
|
-
import { findProjectRoot } from "./projectRoot.js";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Create a backup copy of a file before modifying it.
|
|
19
|
-
* Keeps a rolling window of the last 5 backups per file in a centralized directory.
|
|
20
|
-
*
|
|
21
|
-
* Backups are stored at: [project-root]/.mcp-backups/[relative-path]/[filename].N.backup
|
|
22
|
-
*/
|
|
23
|
-
export function createBackup(filePath: string): string {
|
|
24
|
-
if (!fs.existsSync(filePath)) return "";
|
|
25
|
-
|
|
26
|
-
// Find project root
|
|
27
|
-
const projectRoot = findProjectRoot(filePath);
|
|
28
|
-
if (!projectRoot) {
|
|
29
|
-
// Fallback to old behavior if no project root found
|
|
30
|
-
return createBackupLegacy(filePath);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Calculate relative path from project root
|
|
34
|
-
const relativePath = path.relative(projectRoot, filePath);
|
|
35
|
-
const parsed = path.parse(relativePath);
|
|
36
|
-
|
|
37
|
-
// Create centralized backup directory
|
|
38
|
-
const backupDir = path.join(projectRoot, ".mcp-backups", parsed.dir);
|
|
39
|
-
|
|
40
|
-
if (!fs.existsSync(backupDir)) {
|
|
41
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Push existing backups down the stack (max 5)
|
|
45
|
-
for (let i = 4; i >= 1; i--) {
|
|
46
|
-
const oldB = path.join(backupDir, `${parsed.base}.${i}.backup`);
|
|
47
|
-
const newB = path.join(backupDir, `${parsed.base}.${i + 1}.backup`);
|
|
48
|
-
if (fs.existsSync(oldB)) {
|
|
49
|
-
if (i === 4 && fs.existsSync(newB)) {
|
|
50
|
-
fs.unlinkSync(newB); // Drop the 5th one to make room
|
|
51
|
-
}
|
|
52
|
-
fs.renameSync(oldB, newB);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const backupPath = path.join(backupDir, `${parsed.base}.1.backup`);
|
|
57
|
-
fs.copyFileSync(filePath, backupPath);
|
|
58
|
-
return backupPath;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Legacy backup function (creates .mcp-backups in same directory)
|
|
63
|
-
* Used as fallback when project root cannot be determined
|
|
64
|
-
*/
|
|
65
|
-
function createBackupLegacy(filePath: string): string {
|
|
66
|
-
const parsed = path.parse(filePath);
|
|
67
|
-
const backupDir = path.join(parsed.dir, ".mcp-backups");
|
|
68
|
-
|
|
69
|
-
if (!fs.existsSync(backupDir)) {
|
|
70
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (let i = 4; i >= 1; i--) {
|
|
74
|
-
const oldB = path.join(backupDir, `${parsed.base}.${i}.backup`);
|
|
75
|
-
const newB = path.join(backupDir, `${parsed.base}.${i + 1}.backup`);
|
|
76
|
-
if (fs.existsSync(oldB)) {
|
|
77
|
-
if (i === 4 && fs.existsSync(newB)) {
|
|
78
|
-
fs.unlinkSync(newB);
|
|
79
|
-
}
|
|
80
|
-
fs.renameSync(oldB, newB);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const backupPath = path.join(backupDir, `${parsed.base}.1.backup`);
|
|
85
|
-
fs.copyFileSync(filePath, backupPath);
|
|
86
|
-
return backupPath;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Restore a file from its backup.
|
|
91
|
-
*/
|
|
92
|
-
export function restoreBackup(filePath: string, steps = 1): boolean {
|
|
93
|
-
// Try centralized backup first
|
|
94
|
-
const projectRoot = findProjectRoot(filePath);
|
|
95
|
-
|
|
96
|
-
if (projectRoot) {
|
|
97
|
-
const relativePath = path.relative(projectRoot, filePath);
|
|
98
|
-
const parsed = path.parse(relativePath);
|
|
99
|
-
const backupDir = path.join(projectRoot, ".mcp-backups", parsed.dir);
|
|
100
|
-
const targetBackup = path.join(backupDir, `${parsed.base}.${steps}.backup`);
|
|
101
|
-
|
|
102
|
-
if (fs.existsSync(targetBackup)) {
|
|
103
|
-
fs.copyFileSync(targetBackup, filePath);
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Fallback to legacy location
|
|
109
|
-
const parsed = path.parse(filePath);
|
|
110
|
-
const backupDir = path.join(parsed.dir, ".mcp-backups");
|
|
111
|
-
const targetBackup = path.join(backupDir, `${parsed.base}.${steps}.backup`);
|
|
112
|
-
|
|
113
|
-
if (!fs.existsSync(targetBackup)) return false;
|
|
114
|
-
|
|
115
|
-
fs.copyFileSync(targetBackup, filePath);
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Clean all backups for a project.
|
|
121
|
-
* Removes the entire .mcp-backups directory at project root.
|
|
122
|
-
*/
|
|
123
|
-
export function cleanAllBackups(projectRoot: string): boolean {
|
|
124
|
-
const backupDir = path.join(projectRoot, ".mcp-backups");
|
|
125
|
-
|
|
126
|
-
if (!fs.existsSync(backupDir)) return false;
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
130
|
-
return true;
|
|
131
|
-
} catch {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Clean legacy backups (no-op, kept for compatibility).
|
|
138
|
-
*/
|
|
139
|
-
export function cleanBackup(filePath: string): void {
|
|
140
|
-
// No-op. We keep the rolling 5 versions instead of deleting.
|
|
141
|
-
}
|
|
@@ -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();
|
package/src/utils/diffEngine.ts
DELETED
|
@@ -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
|
-
}
|