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.
Files changed (128) hide show
  1. package/.io/RULES.md +12 -0
  2. package/CHANGELOG.md +88 -1
  3. package/README.md +27 -0
  4. package/RELEASE_NOTES_v2.3.0.md +228 -0
  5. package/RELEASE_NOTES_v2.3.1.md +110 -0
  6. package/dist/src/ast/writers/safeRename.d.ts +14 -0
  7. package/dist/src/ast/writers/safeRename.js +258 -0
  8. package/dist/src/ast/writers/safeRename.js.map +1 -0
  9. package/dist/src/ast/writers/symbolWriter.d.ts +2 -0
  10. package/dist/src/ast/writers/symbolWriter.js +9 -14
  11. package/dist/src/ast/writers/symbolWriter.js.map +1 -1
  12. package/dist/src/cache/astCache.d.ts +33 -0
  13. package/dist/src/cache/astCache.js +113 -0
  14. package/dist/src/cache/astCache.js.map +1 -0
  15. package/dist/src/handlers/readHandlers.d.ts +70 -0
  16. package/dist/src/handlers/readHandlers.js +431 -0
  17. package/dist/src/handlers/readHandlers.js.map +1 -0
  18. package/dist/src/handlers/utilHandlers.d.ts +31 -0
  19. package/dist/src/handlers/utilHandlers.js +69 -0
  20. package/dist/src/handlers/utilHandlers.js.map +1 -0
  21. package/dist/src/handlers/writeHandlers.d.ts +57 -0
  22. package/dist/src/handlers/writeHandlers.js +475 -0
  23. package/dist/src/handlers/writeHandlers.js.map +1 -0
  24. package/dist/src/index.d.ts +6 -143
  25. package/dist/src/index.js +77 -1091
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/utils/arrayUtils.d.ts +12 -0
  28. package/dist/src/utils/arrayUtils.js +27 -0
  29. package/dist/src/utils/arrayUtils.js.map +1 -0
  30. package/dist/src/utils/constants.d.ts +46 -0
  31. package/dist/src/utils/constants.js +112 -0
  32. package/dist/src/utils/constants.js.map +1 -0
  33. package/dist/src/utils/ignoreManager.d.ts +8 -0
  34. package/dist/src/utils/ignoreManager.js +49 -0
  35. package/dist/src/utils/ignoreManager.js.map +1 -1
  36. package/dist/src/utils/normalization.d.ts +49 -0
  37. package/dist/src/utils/normalization.js +173 -0
  38. package/dist/src/utils/normalization.js.map +1 -0
  39. package/dist/src/utils/projectRoot.d.ts +12 -5
  40. package/dist/src/utils/projectRoot.js +22 -20
  41. package/dist/src/utils/projectRoot.js.map +1 -1
  42. package/dist/src/utils/secureValidation.d.ts +15 -0
  43. package/dist/src/utils/secureValidation.js +41 -0
  44. package/dist/src/utils/secureValidation.js.map +1 -0
  45. package/dist/src/utils/transactionManager.d.ts +45 -0
  46. package/dist/src/utils/transactionManager.js +102 -0
  47. package/dist/src/utils/transactionManager.js.map +1 -0
  48. package/dist/src/utils/validation.d.ts +87 -0
  49. package/dist/src/utils/validation.js +231 -0
  50. package/dist/src/utils/validation.js.map +1 -0
  51. package/docs/ARCHITECTURE.md +300 -0
  52. package/docs/architecture/ADR-001-modular-handlers.md +119 -0
  53. package/package.json +5 -5
  54. package/AGENT_INSTRUCTIONS.md +0 -135
  55. package/DEPLOYMENT_INSTRUCTIONS.md +0 -110
  56. package/MEJORAS_PROPUESTAS.md +0 -324
  57. package/dist/src/tests/test-confirmation-flow.d.ts +0 -1
  58. package/dist/src/tests/test-confirmation-flow.js +0 -43
  59. package/dist/src/tests/test-confirmation-flow.js.map +0 -1
  60. package/dist/tests/debug-replace.d.ts +0 -1
  61. package/dist/tests/debug-replace.js +0 -17
  62. package/dist/tests/debug-replace.js.map +0 -1
  63. package/dist/tests/test-backup-system.d.ts +0 -7
  64. package/dist/tests/test-backup-system.js +0 -152
  65. package/dist/tests/test-backup-system.js.map +0 -1
  66. package/dist/tests/test-confirmation-flow.d.ts +0 -1
  67. package/dist/tests/test-confirmation-flow.js +0 -43
  68. package/dist/tests/test-confirmation-flow.js.map +0 -1
  69. package/dist/tests/test-dart.d.ts +0 -1
  70. package/dist/tests/test-dart.js +0 -242
  71. package/dist/tests/test-dart.js.map +0 -1
  72. package/dist/tests/test-new-tools.d.ts +0 -4
  73. package/dist/tests/test-new-tools.js +0 -147
  74. package/dist/tests/test-new-tools.js.map +0 -1
  75. package/dist/tests/test-php-ast.d.ts +0 -1
  76. package/dist/tests/test-php-ast.js +0 -152
  77. package/dist/tests/test-php-ast.js.map +0 -1
  78. package/dist/tests/test-php.d.ts +0 -1
  79. package/dist/tests/test-php.js +0 -234
  80. package/dist/tests/test-php.js.map +0 -1
  81. package/dist/tests/test-smoke.d.ts +0 -5
  82. package/dist/tests/test-smoke.js +0 -83
  83. package/dist/tests/test-smoke.js.map +0 -1
  84. package/dist/tests/writers/test-crlf-handling.d.ts +0 -4
  85. package/dist/tests/writers/test-crlf-handling.js +0 -73
  86. package/dist/tests/writers/test-crlf-handling.js.map +0 -1
  87. package/dist/tests/writers/test-write-smoke.d.ts +0 -9
  88. package/dist/tests/writers/test-write-smoke.js +0 -346
  89. package/dist/tests/writers/test-write-smoke.js.map +0 -1
  90. package/src/ast/dartCompressor.ts +0 -1013
  91. package/src/ast/phpCompressor.ts +0 -814
  92. package/src/ast/semanticCompressor.ts +0 -808
  93. package/src/ast/writers/dartWriter.ts +0 -648
  94. package/src/ast/writers/phpWriter.ts +0 -508
  95. package/src/ast/writers/pyWriter.ts +0 -446
  96. package/src/ast/writers/symbolWriter.ts +0 -277
  97. package/src/ast/writers/tsWriter.ts +0 -418
  98. package/src/index.ts +0 -1784
  99. package/src/tools/readFileLines.ts +0 -104
  100. package/src/tools/searchCodePattern.ts +0 -129
  101. package/src/utils/backupManager.ts +0 -141
  102. package/src/utils/confirmationCache.ts +0 -61
  103. package/src/utils/diffEngine.ts +0 -223
  104. package/src/utils/fuzzyMatch.ts +0 -86
  105. package/src/utils/ignoreManager.ts +0 -144
  106. package/src/utils/projectRoot.ts +0 -44
  107. package/tests/test-backup-system.ts +0 -174
  108. package/tests/test-confirmation-flow.ts +0 -49
  109. package/tests/test-dart.d.ts +0 -1
  110. package/tests/test-dart.js +0 -209
  111. package/tests/test-dart.js.map +0 -1
  112. package/tests/test-dart.ts +0 -265
  113. package/tests/test-new-tools.ts +0 -165
  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,508 +0,0 @@
1
- /**
2
- * phpWriter.ts — PHP Surgical Writer
3
- *
4
- * Uses php-parser to parse the AST for symbol location data,
5
- * then performs line-splice operations on the raw source text.
6
- * php-parser doesn't have a write API like ts-morph, so we locate
7
- * symbols via AST → get line numbers → splice in the replacement.
8
- *
9
- * Supports: .php, .phtml
10
- */
11
-
12
- import { createRequire } from "node:module";
13
-
14
- // ─── Types ──────────────────────────────────────────────────────────
15
-
16
- export interface WriteResult {
17
- success: boolean;
18
- newContent: string;
19
- symbolsAffected: string[];
20
- error?: string;
21
- }
22
-
23
- export type InsertPosition = "before" | "after" | "inside_start" | "inside_end";
24
-
25
- interface PhpNode {
26
- kind: string;
27
- name?: string | { name: string; kind: string };
28
- children?: PhpNode[];
29
- body?: PhpNode[] | PhpNode;
30
- items?: PhpNode[];
31
- loc?: {
32
- start: { line: number; column: number; offset: number };
33
- end: { line: number; column: number; offset: number };
34
- };
35
- leadingComments?: Array<{ kind: string; value: string }>;
36
- }
37
-
38
- // ─── Parser ─────────────────────────────────────────────────────────
39
-
40
- let _parser: any = null;
41
-
42
- function getParser(): any {
43
- if (!_parser) {
44
- const require = createRequire(import.meta.url);
45
- const phpParser = require("php-parser");
46
- _parser = new phpParser.Engine({
47
- parser: {
48
- extractDoc: true,
49
- php7: true,
50
- locations: true,
51
- suppressErrors: true,
52
- },
53
- ast: { withPositions: true },
54
- });
55
- }
56
- return _parser;
57
- }
58
-
59
- function parsePhp(content: string): PhpNode | null {
60
- try {
61
- return getParser().parseCode(content, "file.php");
62
- } catch {
63
- return null;
64
- }
65
- }
66
-
67
- // ─── Replace Symbol ─────────────────────────────────────────────────
68
-
69
- /**
70
- * Replace the full text of a named symbol in a PHP file.
71
- * When className is provided, scopes the search to within that class only.
72
- */
73
- export function replacePHPSymbol(
74
- content: string,
75
- symbolName: string,
76
- newContent: string,
77
- className?: string,
78
- ): WriteResult {
79
- const ast = parsePhp(content);
80
- if (!ast) {
81
- return {
82
- success: false,
83
- newContent: content,
84
- symbolsAffected: [],
85
- error: "Failed to parse PHP file",
86
- };
87
- }
88
-
89
- const node = findSymbolNode(ast, symbolName, className);
90
- if (!node || !node.loc) {
91
- return {
92
- success: false,
93
- newContent: content,
94
- symbolsAffected: [],
95
- error: className
96
- ? `Symbol "${symbolName}" not found in class "${className}" in PHP file`
97
- : `Symbol "${symbolName}" not found in PHP file`,
98
- };
99
- }
100
-
101
- const lines = content.split("\n");
102
-
103
- // Find the actual start including doc comments
104
- const startLine = findActualStart(lines, node);
105
- const endLine = node.loc.end.line; // 1-indexed, inclusive
106
-
107
- // Detect the indentation of the original symbol
108
- const originalIndent = getIndent(lines[startLine - 1] || "");
109
-
110
- // Re-indent the new content to match
111
- const reindented = reindentCode(newContent.trim(), originalIndent);
112
-
113
- // Splice in the replacement
114
- const before = lines.slice(0, startLine - 1);
115
- const after = lines.slice(endLine);
116
- const result = [...before, reindented, ...after].join("\n");
117
-
118
- // Validate: try to parse the result
119
- const validationAst = parsePhp(result);
120
- if (!validationAst) {
121
- return {
122
- success: false,
123
- newContent: content,
124
- symbolsAffected: [],
125
- error: `Replacement produced invalid PHP syntax. Aborting.`,
126
- };
127
- }
128
-
129
- return {
130
- success: true,
131
- newContent: result,
132
- symbolsAffected: [symbolName],
133
- };
134
- }
135
-
136
- // ─── Insert Code ────────────────────────────────────────────────────
137
-
138
- /**
139
- * Insert new code at a precise location relative to an anchor symbol.
140
- * When className is provided, scopes the anchor search to within that class only.
141
- */
142
- export function insertPHPCode(
143
- content: string,
144
- code: string,
145
- anchorSymbol: string | null,
146
- position: InsertPosition,
147
- className?: string,
148
- ): WriteResult {
149
- const lines = content.split("\n");
150
-
151
- if (!anchorSymbol) {
152
- // No anchor — insert before the final closing tag or at end
153
- const lastClosingBrace = findLastClosingBrace(lines);
154
- if (lastClosingBrace !== -1) {
155
- // Insert before the last } (likely end of namespace or class)
156
- lines.splice(lastClosingBrace, 0, "", code.trim(), "");
157
- } else {
158
- lines.push("", code.trim());
159
- }
160
- return {
161
- success: true,
162
- newContent: lines.join("\n"),
163
- symbolsAffected: [],
164
- };
165
- }
166
-
167
- const ast = parsePhp(content);
168
- if (!ast) {
169
- return {
170
- success: false,
171
- newContent: content,
172
- symbolsAffected: [],
173
- error: "Failed to parse PHP file",
174
- };
175
- }
176
-
177
- const node = findSymbolNode(ast, anchorSymbol, className);
178
- if (!node || !node.loc) {
179
- return {
180
- success: false,
181
- newContent: content,
182
- symbolsAffected: [],
183
- error: className
184
- ? `Anchor symbol "${anchorSymbol}" not found in class "${className}"`
185
- : `Anchor symbol "${anchorSymbol}" not found`,
186
- };
187
- }
188
-
189
- const startLine = findActualStart(lines, node);
190
- const endLine = node.loc.end.line;
191
-
192
- const indent = getIndent(lines[startLine - 1] || "");
193
- const reindented = reindentCode(code.trim(), indent);
194
-
195
- switch (position) {
196
- case "before":
197
- lines.splice(startLine - 1, 0, reindented, "");
198
- break;
199
- case "after":
200
- lines.splice(endLine, 0, "", reindented);
201
- break;
202
- case "inside_start": {
203
- // Find the opening brace of the class/interface
204
- const braceIdx = findOpeningBrace(lines, startLine - 1);
205
- if (braceIdx === -1) {
206
- return {
207
- success: false,
208
- newContent: content,
209
- symbolsAffected: [],
210
- error: `Cannot find opening brace for "${anchorSymbol}"`,
211
- };
212
- }
213
- const innerIndent = indent + " ";
214
- const innerCode = reindentCode(code.trim(), innerIndent);
215
- lines.splice(braceIdx + 1, 0, innerCode, "");
216
- break;
217
- }
218
- case "inside_end": {
219
- // Insert before the closing brace
220
- const innerIndent = indent + " ";
221
- const innerCode = reindentCode(code.trim(), innerIndent);
222
- lines.splice(endLine - 1, 0, "", innerCode);
223
- break;
224
- }
225
- }
226
-
227
- return {
228
- success: true,
229
- newContent: lines.join("\n"),
230
- symbolsAffected: [anchorSymbol],
231
- };
232
- }
233
-
234
- // ─── Rename Symbol ──────────────────────────────────────────────────
235
-
236
- /**
237
- * Rename all occurrences of a symbol within a PHP file.
238
- * Performs whole-word replacement respecting PHP identifier boundaries.
239
- */
240
- export function renamePHPSymbol(
241
- content: string,
242
- oldName: string,
243
- newName: string,
244
- ): WriteResult {
245
- const ast = parsePhp(content);
246
- if (!ast) {
247
- return {
248
- success: false,
249
- newContent: content,
250
- symbolsAffected: [],
251
- error: "Failed to parse PHP file",
252
- };
253
- }
254
-
255
- // Verify the symbol exists
256
- const node = findSymbolNode(ast, oldName);
257
- if (!node) {
258
- return {
259
- success: false,
260
- newContent: content,
261
- symbolsAffected: [],
262
- error: `Symbol "${oldName}" not found for renaming`,
263
- };
264
- }
265
-
266
- // Perform whole-word replacement
267
- const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
268
- const regex = new RegExp(`\\b${escaped}\\b`, "g");
269
- const result = content.replace(regex, newName);
270
-
271
- // Validate
272
- const validationAst = parsePhp(result);
273
- if (!validationAst) {
274
- return {
275
- success: false,
276
- newContent: content,
277
- symbolsAffected: [],
278
- error: `Rename produced invalid PHP syntax`,
279
- };
280
- }
281
-
282
- return {
283
- success: true,
284
- newContent: result,
285
- symbolsAffected: [oldName],
286
- };
287
- }
288
-
289
- // ─── Remove Symbol ──────────────────────────────────────────────────
290
-
291
- /**
292
- * Remove a named symbol from a PHP file.
293
- * When className is provided, scopes the search to within that class only.
294
- */
295
- export function removePHPSymbol(
296
- content: string,
297
- symbolName: string,
298
- className?: string,
299
- ): WriteResult {
300
- const ast = parsePhp(content);
301
- if (!ast) {
302
- return {
303
- success: false,
304
- newContent: content,
305
- symbolsAffected: [],
306
- error: "Failed to parse PHP file",
307
- };
308
- }
309
-
310
- const node = findSymbolNode(ast, symbolName, className);
311
- if (!node || !node.loc) {
312
- return {
313
- success: false,
314
- newContent: content,
315
- symbolsAffected: [],
316
- error: className
317
- ? `Symbol "${symbolName}" not found in class "${className}" for removal`
318
- : `Symbol "${symbolName}" not found for removal`,
319
- };
320
- }
321
-
322
- const lines = content.split("\n");
323
- const startLine = findActualStart(lines, node);
324
- const endLine = node.loc.end.line;
325
-
326
- // Remove the lines
327
- lines.splice(startLine - 1, endLine - startLine + 1);
328
-
329
- // Clean up consecutive blank lines
330
- let result = lines.join("\n");
331
- result = result.replace(/\n{3,}/g, "\n\n");
332
-
333
- return {
334
- success: true,
335
- newContent: result,
336
- symbolsAffected: [symbolName],
337
- };
338
- }
339
-
340
- // ─── Internal Helpers ───────────────────────────────────────────────
341
-
342
- function getNodeName(node: PhpNode | undefined | null): string {
343
- if (!node || !node.name) return "";
344
- if (typeof node.name === "string") return node.name;
345
- if (typeof node.name === "object" && "name" in node.name)
346
- return node.name.name;
347
- return "";
348
- }
349
-
350
- /**
351
- * Find a class/interface/trait/enum node by name in the AST tree.
352
- * Used to scope symbol searches to a specific container.
353
- */
354
- function findClassNodePhp(node: PhpNode, className: string): PhpNode | null {
355
- if (!node) return null;
356
-
357
- if (
358
- ["class", "interface", "trait", "enum"].includes(node.kind) &&
359
- getNodeName(node) === className
360
- ) {
361
- return node;
362
- }
363
-
364
- const allChildren = [
365
- ...(node.children || []),
366
- ...(Array.isArray(node.body) ? node.body : []),
367
- ];
368
-
369
- for (const child of allChildren) {
370
- const found = findClassNodePhp(child, className);
371
- if (found) return found;
372
- }
373
-
374
- return null;
375
- }
376
-
377
- function findSymbolNode(
378
- node: PhpNode,
379
- symbolName: string,
380
- className?: string,
381
- ): PhpNode | null {
382
- if (!node) return null;
383
-
384
- // If className is provided, first find the class, then search within it
385
- if (className) {
386
- const classNode = findClassNodePhp(node, className);
387
- if (!classNode) return null;
388
-
389
- // Search only within this class's body
390
- const body = Array.isArray(classNode.body) ? classNode.body : [];
391
- for (const member of body) {
392
- if (member.kind === "method" && getNodeName(member) === symbolName) {
393
- return member;
394
- }
395
- }
396
- return null;
397
- }
398
-
399
- const nodeName = getNodeName(node);
400
-
401
- // Direct match on class, interface, trait, enum, function
402
- if (
403
- ["class", "interface", "trait", "enum", "function"].includes(node.kind) &&
404
- nodeName === symbolName
405
- ) {
406
- return node;
407
- }
408
-
409
- // Search methods inside classes/interfaces/traits/enums
410
- if (["class", "interface", "trait", "enum"].includes(node.kind)) {
411
- const body = Array.isArray(node.body) ? node.body : [];
412
- for (const member of body) {
413
- if (member.kind === "method" && getNodeName(member) === symbolName) {
414
- return member;
415
- }
416
- }
417
- }
418
-
419
- // Recursive search through children
420
- const allChildren = [
421
- ...(node.children || []),
422
- ...(Array.isArray(node.body) ? node.body : []),
423
- ];
424
-
425
- for (const child of allChildren) {
426
- const found = findSymbolNode(child, symbolName);
427
- if (found) return found;
428
- }
429
-
430
- return null;
431
- }
432
-
433
- /**
434
- * Walk backward from a symbol's start to capture leading doc comments.
435
- * Returns the 1-indexed line number of the actual start.
436
- */
437
- function findActualStart(lines: string[], node: PhpNode): number {
438
- if (!node.loc) return 1;
439
-
440
- let startLine = node.loc.start.line; // 1-indexed
441
-
442
- if (node.leadingComments && node.leadingComments.length > 0) {
443
- for (let i = startLine - 2; i >= 0; i--) {
444
- const trimmed = lines[i].trim();
445
- if (
446
- trimmed.startsWith("/**") ||
447
- trimmed.startsWith("*") ||
448
- trimmed.startsWith("*/") ||
449
- trimmed.startsWith("//") ||
450
- trimmed.startsWith("#[")
451
- ) {
452
- startLine = i + 1; // Convert back to 1-indexed
453
- } else if (trimmed === "") {
454
- continue;
455
- } else {
456
- break;
457
- }
458
- }
459
- }
460
-
461
- return startLine;
462
- }
463
-
464
- function getIndent(line: string): string {
465
- const match = line.match(/^(\s*)/);
466
- return match ? match[1] : "";
467
- }
468
-
469
- /**
470
- * Re-indent a block of code to match a target indentation level.
471
- * Detects the minimum indentation of the input and replaces it with target.
472
- */
473
- function reindentCode(code: string, targetIndent: string): string {
474
- const lines = code.split("\n");
475
- if (lines.length === 0) return code;
476
-
477
- // Find minimum non-empty indentation
478
- let minIndent = Infinity;
479
- for (const line of lines) {
480
- if (line.trim() === "") continue;
481
- const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
482
- if (indent < minIndent) minIndent = indent;
483
- }
484
-
485
- if (minIndent === Infinity) minIndent = 0;
486
-
487
- // Re-indent each line
488
- return lines
489
- .map((line) => {
490
- if (line.trim() === "") return "";
491
- return targetIndent + line.slice(minIndent);
492
- })
493
- .join("\n");
494
- }
495
-
496
- function findOpeningBrace(lines: string[], startIdx: number): number {
497
- for (let i = startIdx; i < lines.length; i++) {
498
- if (lines[i].includes("{")) return i;
499
- }
500
- return -1;
501
- }
502
-
503
- function findLastClosingBrace(lines: string[]): number {
504
- for (let i = lines.length - 1; i >= 0; i--) {
505
- if (lines[i].trim() === "}") return i;
506
- }
507
- return -1;
508
- }