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
package/dist/src/index.js CHANGED
@@ -1,20 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * mcp-code-context — Semantic Code Context Server
3
+ * mcp-code-context — Semantic Code Context Server (v2.3.1 - Refactored)
4
4
  *
5
5
  * A Model Context Protocol (MCP) server that compresses any code repository
6
6
  * into LLM-ready semantic context and provides surgical code editing.
7
7
  *
8
- * Read Tools:
9
- * 1. get_semantic_repo_map — Compressed structural map of a repo
10
- * 2. read_file_surgical — Full file or specific symbol extraction
11
- * 3. analyze_impact — Find all dependents of a modified file
12
- *
13
- * Write Tools:
14
- * 4. write_file_surgical — Replace a symbol's code in-place
15
- * 5. insert_symbol — Insert code at precise AST positions
16
- * 6. rename_symbol — Rename across entire repository
17
- * 7. remove_symbol — Safely delete symbols with dep checking
8
+ * Architecture: Modular handlers with centralized utilities
9
+ * - Read handlers: get_semantic_repo_map, read_file_surgical, analyze_impact, etc.
10
+ * - Write handlers: write_file_surgical, insert_symbol, rename_symbol, remove_symbol
11
+ * - Util handlers: rollback_file, clean_backups
18
12
  *
19
13
  * Transport: stdio (JSON-RPC over stdin/stdout)
20
14
  * Compatible with: Claude Desktop, Cursor, Windsurf, Copilot, Amazon Q, and any MCP client.
@@ -22,43 +16,11 @@
22
16
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
23
17
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
18
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
25
- import { IgnoreManager } from "./utils/ignoreManager.js";
26
- import { compressFile, extractSymbol } from "./ast/semanticCompressor.js";
27
- import { replaceSymbol, insertCode, renameInFile, renameReferencesInFile, removeSymbolFromFile, WRITABLE_EXTENSIONS, } from "./ast/writers/symbolWriter.js";
28
- import { generateDiff, generateMultiFileDiff } from "./utils/diffEngine.js";
29
- import { createBackup, restoreBackup } from "./utils/backupManager.js";
30
- import { extractFallbackSymbols, findClosestSymbols } from "./utils/fuzzyMatch.js";
31
- import { confirmationCache } from "./utils/confirmationCache.js";
32
- import * as fs from "node:fs";
33
- import * as path from "node:path";
34
- // ─── Constants ──────────────────────────────────────────────────────
35
- const SERVER_NAME = "mcp-code-context";
36
- const SERVER_VERSION = "2.0.0";
37
- /** Extensions we consider source code for analysis */
38
- const SOURCE_EXTENSIONS = new Set([
39
- ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
40
- ".py", ".pyi",
41
- ".json", ".yaml", ".yml", ".toml",
42
- ".md", ".txt",
43
- ".css", ".scss", ".less",
44
- ".html", ".vue", ".svelte",
45
- ".rs", ".go", ".java", ".kt", ".dart",
46
- ".c", ".cpp", ".h", ".hpp",
47
- ".rb", ".php", ".swift",
48
- ".sh", ".bash", ".zsh",
49
- ".sql",
50
- ".graphql", ".gql",
51
- ".proto",
52
- ".dockerfile",
53
- ]);
54
- /** Extensions eligible for import analysis */
55
- const IMPORTABLE_EXTENSIONS = new Set([
56
- ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
57
- ".py", ".pyi",
58
- ".php", ".phtml",
59
- ".dart",
60
- ".vue", ".svelte",
61
- ]);
19
+ // Import handlers
20
+ import { handleGetSemanticRepoMap, handleReadFileSurgical, handleAnalyzeImpact, handleReadFileLines, handleSearchCodePattern, } from "./handlers/readHandlers.js";
21
+ import { handleWriteFileSurgical, handleInsertSymbol, handleRenameSymbol, handleRemoveSymbol, } from "./handlers/writeHandlers.js";
22
+ import { handleRollbackFile, handleCleanBackups, } from "./handlers/utilHandlers.js";
23
+ import { SERVER_NAME, SERVER_VERSION } from "./utils/constants.js";
62
24
  // ─── MCP Server Instance ────────────────────────────────────────────
63
25
  const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
64
26
  // ─── Tool Definitions ───────────────────────────────────────────────
@@ -118,24 +80,6 @@ const TOOLS = [
118
80
  required: ["filePath"],
119
81
  },
120
82
  },
121
- {
122
- name: "rollback_file",
123
- description: "Reverts a file to a previous backup state after a surgical write operation. Keeps up to 5 automatic backups. Specify the number of steps to rollback (1 = most recent).",
124
- inputSchema: {
125
- type: "object",
126
- properties: {
127
- filePath: {
128
- type: "string",
129
- description: "Absolute path to the source file to revert.",
130
- },
131
- steps: {
132
- type: "number",
133
- description: "Number of steps back to revert to (1-5). Defaults to 1 (most recent backup).",
134
- }
135
- },
136
- required: ["filePath"],
137
- },
138
- },
139
83
  {
140
84
  name: "analyze_impact",
141
85
  description: "Analyzes the impact of modifying a given file by discovering all other " +
@@ -161,7 +105,87 @@ const TOOLS = [
161
105
  required: ["filePath"],
162
106
  },
163
107
  },
164
- // ─── Write Tools ────────────────────────────────────────────────────
108
+ {
109
+ name: "read_file_lines",
110
+ description: "Reads specific line ranges from a file without loading the entire content. " +
111
+ "Supports reading by exact line range (startLine/endLine) or around a pattern match. " +
112
+ "More efficient than read_file_surgical when you only need a small fragment of code. " +
113
+ "Perfect for debugging, viewing specific code blocks, or extracting context around errors.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ filePath: {
118
+ type: "string",
119
+ description: "Absolute path to the source file to read.",
120
+ },
121
+ startLine: {
122
+ type: "number",
123
+ description: "Starting line number (1-indexed). Required if using exact range mode.",
124
+ },
125
+ endLine: {
126
+ type: "number",
127
+ description: "Ending line number (1-indexed). Required if using exact range mode.",
128
+ },
129
+ aroundPattern: {
130
+ type: "string",
131
+ description: "Search pattern to find in the file. Returns lines around the first match. " +
132
+ "Use this mode when you don't know the exact line numbers.",
133
+ },
134
+ contextLines: {
135
+ type: "number",
136
+ description: "Number of lines to include before and after the pattern match. " +
137
+ "Defaults to 5. Only used with aroundPattern mode.",
138
+ },
139
+ },
140
+ required: ["filePath"],
141
+ },
142
+ },
143
+ {
144
+ name: "search_code_pattern",
145
+ description: "Searches for code patterns across multiple files in a repository. " +
146
+ "Returns matches with file paths, line numbers, and optional context lines. " +
147
+ "Respects .gitignore rules and allows filtering by file extensions. " +
148
+ "More efficient than manual grep when you need structured results with context.",
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: {
152
+ rootDir: {
153
+ type: "string",
154
+ description: "Absolute path to the repository root directory to search.",
155
+ },
156
+ pattern: {
157
+ type: "string",
158
+ description: "Regular expression pattern to search for. Use proper regex escaping " +
159
+ "(e.g., 'widget\\\\.height' to match 'widget.height').",
160
+ },
161
+ fileExtensions: {
162
+ type: "array",
163
+ items: { type: "string" },
164
+ description: "Array of file extensions to search (e.g., ['.ts', '.dart', '.py']). " +
165
+ "Defaults to common code extensions if omitted.",
166
+ },
167
+ excludeDirs: {
168
+ type: "array",
169
+ items: { type: "string" },
170
+ description: "Array of directory names to exclude (e.g., ['node_modules', 'dist']). " +
171
+ "Defaults to common build/dependency directories.",
172
+ },
173
+ showContext: {
174
+ type: "boolean",
175
+ description: "If true, includes surrounding lines for each match. Defaults to true.",
176
+ },
177
+ contextLines: {
178
+ type: "number",
179
+ description: "Number of context lines before/after each match. Defaults to 3.",
180
+ },
181
+ maxResults: {
182
+ type: "number",
183
+ description: "Maximum number of matches to return. Defaults to 50.",
184
+ },
185
+ },
186
+ required: ["rootDir", "pattern"],
187
+ },
188
+ },
165
189
  {
166
190
  name: "write_file_surgical",
167
191
  description: "Surgically replaces the full source code of a named symbol (function, class, " +
@@ -243,9 +267,13 @@ const TOOLS = [
243
267
  description: "Optional. Name of the class to scope the anchor symbol search to. " +
244
268
  "Use when multiple classes have methods with the same name.",
245
269
  },
246
- dryRun: {
270
+ confirmationToken: {
271
+ type: "string",
272
+ description: "Required for Phase 2. The token provided from the Phase 1 Dry-Run preview.",
273
+ },
274
+ confirm: {
247
275
  type: "boolean",
248
- description: "If true, preview changes without modifying the file.",
276
+ description: "Required for Phase 2. Set to true to confirm and apply the changes.",
249
277
  },
250
278
  createBackup: {
251
279
  type: "boolean",
@@ -326,9 +354,13 @@ const TOOLS = [
326
354
  description: "If true, skip the dependency check and remove the symbol even if " +
327
355
  "other files reference it. Defaults to false.",
328
356
  },
329
- dryRun: {
357
+ confirmationToken: {
358
+ type: "string",
359
+ description: "Required for Phase 2. The token provided from the Phase 1 Dry-Run preview.",
360
+ },
361
+ confirm: {
330
362
  type: "boolean",
331
- description: "If true, preview the removal without modifying the file.",
363
+ description: "Required for Phase 2. Set to true to confirm and apply the changes.",
332
364
  },
333
365
  createBackup: {
334
366
  type: "boolean",
@@ -338,6 +370,41 @@ const TOOLS = [
338
370
  required: ["filePath", "symbolName"],
339
371
  },
340
372
  },
373
+ {
374
+ name: "rollback_file",
375
+ description: "Reverts a file to a previous backup state after a surgical write operation. Keeps up to 5 automatic backups. Specify the number of steps to rollback (1 = most recent).",
376
+ inputSchema: {
377
+ type: "object",
378
+ properties: {
379
+ filePath: {
380
+ type: "string",
381
+ description: "Absolute path to the source file to revert.",
382
+ },
383
+ steps: {
384
+ type: "number",
385
+ description: "Number of steps back to revert to (1-5). Defaults to 1 (most recent backup).",
386
+ }
387
+ },
388
+ required: ["filePath"],
389
+ },
390
+ },
391
+ {
392
+ name: "clean_backups",
393
+ description: "Removes all backup files for a project. Deletes the entire .mcp-backups directory " +
394
+ "at the project root, freeing up disk space and cleaning the working directory. " +
395
+ "Use this to clean up after completing a series of edits or when backups are no longer needed.",
396
+ inputSchema: {
397
+ type: "object",
398
+ properties: {
399
+ projectRoot: {
400
+ type: "string",
401
+ description: "Absolute path to the project root directory. The .mcp-backups folder " +
402
+ "at this location will be removed.",
403
+ },
404
+ },
405
+ required: ["projectRoot"],
406
+ },
407
+ },
341
408
  ];
342
409
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
343
410
  tools: TOOLS,
@@ -347,12 +414,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
347
414
  const { name, arguments: args } = request.params;
348
415
  try {
349
416
  switch (name) {
417
+ // Read tools
350
418
  case "get_semantic_repo_map":
351
419
  return await handleGetSemanticRepoMap(args);
352
420
  case "read_file_surgical":
353
421
  return await handleReadFileSurgical(args);
354
422
  case "analyze_impact":
355
423
  return await handleAnalyzeImpact(args);
424
+ case "read_file_lines":
425
+ return await handleReadFileLines(args);
426
+ case "search_code_pattern":
427
+ return await handleSearchCodePattern(args);
428
+ // Write tools
356
429
  case "write_file_surgical":
357
430
  return await handleWriteFileSurgical(args);
358
431
  case "insert_symbol":
@@ -361,8 +434,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
361
434
  return await handleRenameSymbol(args);
362
435
  case "remove_symbol":
363
436
  return await handleRemoveSymbol(args);
437
+ // Util tools
364
438
  case "rollback_file":
365
439
  return await handleRollbackFile(args);
440
+ case "clean_backups":
441
+ return await handleCleanBackups(args);
366
442
  default:
367
443
  return errorResponse(`Unknown tool: "${name}"`);
368
444
  }
@@ -372,901 +448,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
372
448
  return errorResponse(`Unhandled error in tool "${name}": ${message}`);
373
449
  }
374
450
  });
375
- // ─── Tool 1: get_semantic_repo_map ──────────────────────────────────
376
- export async function handleGetSemanticRepoMap(args) {
377
- const directoryPath = args.directoryPath;
378
- const format = args.format || "xml";
379
- if (!directoryPath) {
380
- return errorResponse("Missing required parameter: directoryPath");
381
- }
382
- const resolvedPath = path.resolve(directoryPath);
383
- if (!fs.existsSync(resolvedPath)) {
384
- return errorResponse(`Directory not found: ${resolvedPath}`);
385
- }
386
- const stat = fs.statSync(resolvedPath);
387
- if (!stat.isDirectory()) {
388
- return errorResponse(`Path is not a directory: ${resolvedPath}`);
389
- }
390
- const ignoreManager = new IgnoreManager(resolvedPath);
391
- const files = ignoreManager.walkDirectory();
392
- const projectName = path.basename(resolvedPath);
393
- const entries = [];
394
- let skippedCount = 0;
395
- for (const file of files) {
396
- const ext = path.extname(file).toLowerCase();
397
- if (!SOURCE_EXTENSIONS.has(ext)) {
398
- skippedCount++;
399
- continue;
400
- }
401
- let content;
402
- try {
403
- content = fs.readFileSync(file, "utf-8");
404
- }
405
- catch {
406
- skippedCount++;
407
- continue;
408
- }
409
- // Skip files with null bytes (binary masquerading as text)
410
- if (content.includes("\0")) {
411
- skippedCount++;
412
- continue;
413
- }
414
- const relativePath = path
415
- .relative(resolvedPath, file)
416
- .replace(/\\/g, "/");
417
- const language = getLanguageName(ext);
418
- const compressed = compressFile(file, content);
419
- entries.push({ relativePath, language, compressed });
420
- }
421
- const output = format === "markdown"
422
- ? formatAsMarkdown(projectName, resolvedPath, entries, skippedCount)
423
- : formatAsXml(projectName, resolvedPath, entries, skippedCount);
424
- return {
425
- content: [{ type: "text", text: output }],
426
- };
427
- }
428
- // ─── Tool 2: read_file_surgical ─────────────────────────────────────
429
- export async function handleReadFileSurgical(args) {
430
- const filePath = args.filePath;
431
- const symbolName = args.symbolName;
432
- const className = args.className;
433
- if (!filePath) {
434
- return errorResponse("Missing required parameter: filePath");
435
- }
436
- const resolvedPath = path.resolve(filePath);
437
- if (!fs.existsSync(resolvedPath)) {
438
- return errorResponse(`File not found: ${resolvedPath}`);
439
- }
440
- let content;
441
- try {
442
- content = fs.readFileSync(resolvedPath, "utf-8");
443
- }
444
- catch (error) {
445
- const msg = error instanceof Error ? error.message : String(error);
446
- return errorResponse(`Failed to read file: ${msg}`);
447
- }
448
- // If no symbol requested, return the full file
449
- if (!symbolName) {
450
- return {
451
- content: [
452
- {
453
- type: "text",
454
- text: `// File: ${resolvedPath}\n// Lines: ${content.split("\n").length}\n\n${content}`,
455
- },
456
- ],
457
- };
458
- }
459
- // Attempt surgical extraction
460
- const extracted = extractSymbol(content, symbolName, resolvedPath, className);
461
- if (extracted === null) {
462
- // Symbol not found — use fuzzy matching to return suggestions
463
- const available = extractFallbackSymbols(content);
464
- const closest = findClosestSymbols(symbolName, available, 5);
465
- const availableFormatted = available.slice(0, 15).map(s => `${s.name} (${s.type}${s.className ? ` in ${s.className}` : ''})`);
466
- if (available.length > 15)
467
- availableFormatted.push(`...and ${available.length - 15} more`);
468
- const suggestions = [];
469
- if (className) {
470
- // Check if it exists in another class
471
- const inOtherClass = available.find(s => s.name === symbolName && s.className !== className);
472
- if (inOtherClass)
473
- suggestions.push(`Did you mean '${symbolName}' in class '${inOtherClass.className}'? Update your className parameter.`);
474
- }
475
- else {
476
- // Check if it requires a class scope
477
- const needsScope = available.filter(s => s.name === symbolName && s.className);
478
- if (needsScope.length > 0)
479
- suggestions.push(`Symbol '${symbolName}' exists inside multiple scopes. Did you mean to use className: '${needsScope[0].className}'?`);
480
- }
481
- if (closest.length > 0) {
482
- suggestions.push(`Similar symbols: ${closest.map(s => s.name).join(', ')}`);
483
- }
484
- const errorPayload = {
485
- error: `Symbol '${symbolName}' not found in ${path.basename(resolvedPath)}`,
486
- available_symbols: availableFormatted,
487
- suggestions
488
- };
489
- return {
490
- isError: true,
491
- content: [
492
- {
493
- type: "text",
494
- text: JSON.stringify(errorPayload, null, 2),
495
- },
496
- ],
497
- };
498
- }
499
- return {
500
- content: [
501
- {
502
- type: "text",
503
- text: `// Symbol: ${symbolName}\n` +
504
- `// File: ${resolvedPath}\n\n${extracted}`,
505
- },
506
- ],
507
- };
508
- }
509
- // ─── Tool 3: analyze_impact ─────────────────────────────────────────
510
- export async function handleAnalyzeImpact(args) {
511
- const filePath = args.filePath;
512
- let rootDir = args.rootDir;
513
- if (!filePath) {
514
- return errorResponse("Missing required parameter: filePath");
515
- }
516
- const resolvedPath = path.resolve(filePath);
517
- if (!fs.existsSync(resolvedPath)) {
518
- return errorResponse(`File not found: ${resolvedPath}`);
519
- }
520
- // Discover project root if not provided
521
- if (!rootDir) {
522
- rootDir = findProjectRoot(resolvedPath);
523
- }
524
- if (!rootDir || !fs.existsSync(rootDir)) {
525
- return errorResponse(`Could not determine project root for ${resolvedPath}. ` +
526
- `Provide rootDir explicitly.`);
527
- }
528
- rootDir = path.resolve(rootDir);
529
- const ignoreManager = new IgnoreManager(rootDir);
530
- const allFiles = ignoreManager.walkDirectory();
531
- const targetRelative = path
532
- .relative(rootDir, resolvedPath)
533
- .replace(/\\/g, "/");
534
- // Import pattern regexes
535
- const importPatterns = [
536
- // ES static import: import ... from "path"
537
- /import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/gm,
538
- // ES dynamic import: import("path")
539
- /import\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
540
- // CommonJS: require("path")
541
- /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
542
- // Python: from package import ...
543
- /from\s+(\S+)\s+import/gm,
544
- // Python: import package
545
- /^import\s+(\S+)/gm,
546
- // PHP: use Namespace\Class
547
- /^\s*use\s+([\w\\]+)/gm,
548
- // PHP: require_once / require / include_once / include
549
- /(?:require_once|require|include_once|include)\s*\(?\s*['"]([^'"]+)['"]\s*\)?/gm,
550
- // Dart: import 'package:...' or import '../...';
551
- /^\s*import\s+['"]([^'"]+)['"]\s*;/gm,
552
- ];
553
- const dependents = [];
554
- for (const file of allFiles) {
555
- // Skip the target file itself
556
- if (path.resolve(file) === resolvedPath)
557
- continue;
558
- // Only scan importable file types
559
- const ext = path.extname(file).toLowerCase();
560
- if (!IMPORTABLE_EXTENSIONS.has(ext))
561
- continue;
562
- let content;
563
- try {
564
- content = fs.readFileSync(file, "utf-8");
565
- }
566
- catch {
567
- continue;
568
- }
569
- let isDependent = false;
570
- const matchedImports = [];
571
- for (const pattern of importPatterns) {
572
- const regex = new RegExp(pattern.source, pattern.flags);
573
- let match;
574
- while ((match = regex.exec(content)) !== null) {
575
- const importPath = match[1];
576
- if (resolveImportMatch(importPath, resolvedPath, file, rootDir)) {
577
- matchedImports.push(match[0].trim());
578
- isDependent = true;
579
- }
580
- }
581
- }
582
- if (isDependent) {
583
- dependents.push({
584
- file: path.relative(rootDir, file).replace(/\\/g, "/"),
585
- imports: [...new Set(matchedImports)], // deduplicate
586
- });
587
- }
588
- }
589
- // Build the report
590
- const report = buildImpactReport(targetRelative, rootDir, dependents);
591
- return {
592
- content: [{ type: "text", text: report }],
593
- };
594
- }
595
- // ─── Tool 4: write_file_surgical ────────────────────────────────────
596
- export async function handleWriteFileSurgical(args) {
597
- const filePath = args.filePath;
598
- const symbolName = args.symbolName;
599
- const newContent = args.newContent;
600
- const className = args.className;
601
- const shouldBackup = args.createBackup || false;
602
- const confirmationToken = args.confirmationToken;
603
- const confirm = args.confirm;
604
- if (!filePath)
605
- return errorResponse("Missing required parameter: filePath");
606
- // PHASE 2: Apply the change
607
- if (confirmationToken && confirm) {
608
- const pending = confirmationCache.consume(confirmationToken);
609
- if (!pending || pending.operation !== "write_file_surgical") {
610
- return errorResponse("Invalid or expired confirmation token. Please run the operation again to generate a new preview.");
611
- }
612
- const resolvedPath = pending.filePath;
613
- try {
614
- createBackup(resolvedPath);
615
- }
616
- catch (error) {
617
- return errorResponse(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`);
618
- }
619
- try {
620
- fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
621
- }
622
- catch (error) {
623
- restoreBackup(resolvedPath);
624
- return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
625
- }
626
- return {
627
- content: [
628
- {
629
- type: "text",
630
- text: `// ✅ Symbol "${pending.payload.symbolName}" replaced successfully\n// File: ${resolvedPath}\n\n${pending.payload.diff}`,
631
- },
632
- ],
633
- };
634
- }
635
- // PHASE 1: Dry-Run Preview
636
- if (!symbolName)
637
- return errorResponse("Missing required parameter: symbolName");
638
- if (!newContent)
639
- return errorResponse("Missing required parameter: newContent");
640
- const resolvedPath = path.resolve(filePath);
641
- if (!fs.existsSync(resolvedPath)) {
642
- return errorResponse(`File not found: ${resolvedPath}`);
643
- }
644
- const ext = path.extname(resolvedPath).toLowerCase();
645
- if (!WRITABLE_EXTENSIONS.has(ext)) {
646
- return errorResponse(`Unsupported file type "${ext}" for surgical writing. ` +
647
- `Supported: .ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs, .php, .phtml, .dart, .py, .pyi`);
648
- }
649
- let content;
650
- try {
651
- content = fs.readFileSync(resolvedPath, "utf-8");
652
- }
653
- catch (error) {
654
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
655
- }
656
- // Perform the replacement memory-only
657
- const result = replaceSymbol(resolvedPath, content, symbolName, newContent, className);
658
- if (!result.success) {
659
- const isNotFoundError = result.error && (result.error.includes("not found") || result.error.includes("Could not locate"));
660
- if (isNotFoundError) {
661
- const available = extractFallbackSymbols(content);
662
- const closest = findClosestSymbols(symbolName, available, 5);
663
- const availableFormatted = available.slice(0, 15).map(s => `${s.name} (${s.type}${s.className ? ` in ${s.className}` : ''})`);
664
- if (available.length > 15)
665
- availableFormatted.push(`...and ${available.length - 15} more`);
666
- const suggestions = [];
667
- if (className) {
668
- const inOtherClass = available.find(s => s.name === symbolName && s.className !== className);
669
- if (inOtherClass)
670
- suggestions.push(`Did you mean '${symbolName}' in class '${inOtherClass.className}'? Update your className parameter.`);
671
- }
672
- else {
673
- const needsScope = available.filter(s => s.name === symbolName && s.className);
674
- if (needsScope.length > 0)
675
- suggestions.push(`Symbol '${symbolName}' exists inside multiple scopes. Did you mean to use className: '${needsScope[0].className}'?`);
676
- }
677
- if (closest.length > 0) {
678
- suggestions.push(`Similar symbols: ${closest.map(s => s.name).join(', ')}`);
679
- }
680
- return {
681
- isError: true,
682
- content: [
683
- {
684
- type: "text",
685
- text: JSON.stringify({
686
- error: result.error,
687
- available_symbols: availableFormatted,
688
- suggestions
689
- }, null, 2),
690
- },
691
- ],
692
- };
693
- }
694
- return errorResponse(result.error || "Unknown error during symbol replacement");
695
- }
696
- const diff = generateDiff(content, result.newContent, resolvedPath);
697
- const token = confirmationCache.add("write_file_surgical", resolvedPath, {
698
- symbolName,
699
- newContentFull: result.newContent,
700
- diff,
701
- });
702
- return {
703
- content: [
704
- {
705
- type: "text",
706
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${diff}`,
707
- },
708
- ],
709
- };
710
- }
711
- // ─── Tool 5: insert_symbol ──────────────────────────────────────────
712
- export async function handleInsertSymbol(args) {
713
- const filePath = args.filePath;
714
- const code = args.code;
715
- const anchorSymbol = args.anchorSymbol || null;
716
- const position = args.position || "after";
717
- const className = args.className;
718
- const shouldBackup = args.createBackup || false;
719
- const confirmationToken = args.confirmationToken;
720
- const confirm = args.confirm;
721
- if (!filePath)
722
- return errorResponse("Missing required parameter: filePath");
723
- // PHASE 2: Apply the change
724
- if (confirmationToken && confirm) {
725
- const pending = confirmationCache.consume(confirmationToken);
726
- if (!pending || pending.operation !== "insert_symbol") {
727
- return errorResponse("Invalid or expired confirmation token. Please run the operation again to generate a new preview.");
728
- }
729
- const resolvedPath = pending.filePath;
730
- try {
731
- createBackup(resolvedPath);
732
- }
733
- catch (error) {
734
- return errorResponse(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`);
735
- }
736
- try {
737
- fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
738
- }
739
- catch (error) {
740
- restoreBackup(resolvedPath);
741
- return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
742
- }
743
- return {
744
- content: [
745
- {
746
- type: "text",
747
- text: `// ✅ Code inserted successfully\n// File: ${resolvedPath}\n// Anchor: ${pending.payload.anchorSymbol || "(end of file)"}\n// Position: ${pending.payload.position}\n\n${pending.payload.diff}`,
748
- },
749
- ],
750
- };
751
- }
752
- // PHASE 1: Dry-Run
753
- if (!code)
754
- return errorResponse("Missing required parameter: code");
755
- const resolvedPath = path.resolve(filePath);
756
- if (!fs.existsSync(resolvedPath)) {
757
- return errorResponse(`File not found: ${resolvedPath}`);
758
- }
759
- const ext = path.extname(resolvedPath).toLowerCase();
760
- if (!WRITABLE_EXTENSIONS.has(ext)) {
761
- return errorResponse(`Unsupported file type "${ext}" for code insertion.`);
762
- }
763
- let fileContent;
764
- try {
765
- fileContent = fs.readFileSync(resolvedPath, "utf-8");
766
- }
767
- catch (error) {
768
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
769
- }
770
- const result = insertCode(resolvedPath, fileContent, code, anchorSymbol, position, className);
771
- if (!result.success) {
772
- return errorResponse(result.error || "Unknown error during code insertion");
773
- }
774
- const diff = generateDiff(fileContent, result.newContent, resolvedPath);
775
- const token = confirmationCache.add("insert_symbol", resolvedPath, {
776
- newContentFull: result.newContent,
777
- diff,
778
- anchorSymbol,
779
- position
780
- });
781
- return {
782
- content: [
783
- {
784
- type: "text",
785
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${diff}`,
786
- },
787
- ],
788
- };
789
- }
790
- // ─── Tool 6: rename_symbol ──────────────────────────────────────────
791
- export async function handleRenameSymbol(args) {
792
- const filePath = args.filePath;
793
- const oldName = args.oldName;
794
- const newName = args.newName;
795
- const rootDir = args.rootDir;
796
- const shouldBackup = args.createBackup || false;
797
- const confirmationToken = args.confirmationToken;
798
- const confirm = args.confirm;
799
- if (!filePath)
800
- return errorResponse("Missing required parameter: filePath");
801
- // PHASE 2
802
- if (confirmationToken && confirm) {
803
- const pending = confirmationCache.consume(confirmationToken);
804
- if (!pending || pending.operation !== "rename_symbol") {
805
- return errorResponse("Invalid or expired confirmation token.");
806
- }
807
- const results = pending.payload.results;
808
- for (const res of results) {
809
- try {
810
- createBackup(res.filePath);
811
- }
812
- catch (e) { }
813
- }
814
- try {
815
- for (const res of results) {
816
- fs.writeFileSync(res.filePath, res.newContent, "utf-8");
817
- }
818
- }
819
- catch (error) {
820
- return errorResponse(`Failed to write files during rename: ${error instanceof Error ? error.message : String(error)}`);
821
- }
822
- return {
823
- content: [{
824
- type: "text",
825
- text: `// ✅ Rename applied successfully.\n\n${pending.payload.diff}`,
826
- }],
827
- };
828
- }
829
- // PHASE 1
830
- if (!oldName)
831
- return errorResponse("Missing required parameter: oldName");
832
- if (!newName)
833
- return errorResponse("Missing required parameter: newName");
834
- const resolvedPath = path.resolve(filePath);
835
- if (!fs.existsSync(resolvedPath)) {
836
- return errorResponse(`File not found: ${resolvedPath}`);
837
- }
838
- let sourceContent;
839
- try {
840
- sourceContent = fs.readFileSync(resolvedPath, "utf-8");
841
- }
842
- catch (error) {
843
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
844
- }
845
- // 1. Rename in the source file
846
- const sourceResult = renameInFile(resolvedPath, sourceContent, oldName, newName);
847
- if (!sourceResult.success) {
848
- return errorResponse(sourceResult.error || `Could not find definition for "${oldName}" in ${path.basename(resolvedPath)}.`);
849
- }
850
- const results = [
851
- {
852
- filePath: resolvedPath,
853
- oldContent: sourceContent,
854
- newContent: sourceResult.newContent,
855
- },
856
- ];
857
- // 2. Discover and rename in dependent files
858
- const projectRoot = rootDir ? path.resolve(rootDir) : findProjectRoot(resolvedPath);
859
- if (projectRoot) {
860
- const ignoreManager = new IgnoreManager(projectRoot);
861
- const allFiles = ignoreManager.walkDirectory();
862
- for (const dependentFile of allFiles) {
863
- if (path.resolve(dependentFile) === resolvedPath)
864
- continue;
865
- const ext = path.extname(dependentFile).toLowerCase();
866
- if (!IMPORTABLE_EXTENSIONS.has(ext))
867
- continue;
868
- let depContent;
869
- try {
870
- depContent = fs.readFileSync(dependentFile, "utf-8");
871
- }
872
- catch {
873
- continue;
874
- }
875
- // Quick check if file contains oldName before attempting heavy rename
876
- const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
877
- const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`);
878
- if (!regex.test(depContent))
879
- continue;
880
- const depResult = renameReferencesInFile(dependentFile, depContent, oldName, newName);
881
- if (depResult.success && depResult.newContent !== depContent) {
882
- results.push({
883
- filePath: path.resolve(dependentFile),
884
- oldContent: depContent,
885
- newContent: depResult.newContent,
886
- });
887
- }
888
- }
889
- }
890
- const multiDiff = generateMultiFileDiff(results);
891
- const token = confirmationCache.add("rename_symbol", resolvedPath, {
892
- results,
893
- diff: multiDiff
894
- });
895
- return {
896
- content: [{
897
- type: "text",
898
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${multiDiff}`,
899
- }],
900
- };
901
- }
902
- // ─── Tool 7: remove_symbol ──────────────────────────────────────────
903
- export async function handleRemoveSymbol(args) {
904
- const filePath = args.filePath;
905
- const symbolName = args.symbolName;
906
- const className = args.className;
907
- const force = args.force || false;
908
- const shouldBackup = args.createBackup || false;
909
- const confirmationToken = args.confirmationToken;
910
- const confirm = args.confirm;
911
- if (!filePath)
912
- return errorResponse("Missing required parameter: filePath");
913
- // PHASE 2
914
- if (confirmationToken && confirm) {
915
- const pending = confirmationCache.consume(confirmationToken);
916
- if (!pending || pending.operation !== "remove_symbol") {
917
- return errorResponse("Invalid or expired confirmation token.");
918
- }
919
- const resolvedPath = pending.filePath;
920
- try {
921
- createBackup(resolvedPath);
922
- }
923
- catch (e) { }
924
- try {
925
- fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
926
- }
927
- catch (error) {
928
- restoreBackup(resolvedPath);
929
- return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
930
- }
931
- return {
932
- content: [{
933
- type: "text",
934
- text: `// ✅ Symbol "${pending.payload.symbolName}" removed successfully.\n\n${pending.payload.diff}`,
935
- }],
936
- };
937
- }
938
- if (!symbolName)
939
- return errorResponse("Missing required parameter: symbolName");
940
- const resolvedPath = path.resolve(filePath);
941
- if (!fs.existsSync(resolvedPath)) {
942
- return errorResponse(`File not found: ${resolvedPath}`);
943
- }
944
- let fileContent;
945
- try {
946
- fileContent = fs.readFileSync(resolvedPath, "utf-8");
947
- }
948
- catch (error) {
949
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
950
- }
951
- // Safety check: Don't remove if dependents exist unless forced
952
- if (!force) {
953
- const projectRoot = findProjectRoot(resolvedPath);
954
- if (projectRoot) {
955
- const ignoreManager = new IgnoreManager(projectRoot);
956
- const allFiles = ignoreManager.walkDirectory();
957
- const targetRel = path.relative(projectRoot, resolvedPath).replace(/\\/g, "/");
958
- for (const f of allFiles) {
959
- if (path.resolve(f) === resolvedPath)
960
- continue;
961
- const fExt = path.extname(f).toLowerCase();
962
- if (!IMPORTABLE_EXTENSIONS.has(fExt))
963
- continue;
964
- try {
965
- const fContent = fs.readFileSync(f, "utf-8");
966
- const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
967
- const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`);
968
- if (regex.test(fContent)) {
969
- return errorResponse(`Symbol "${symbolName}" might be used in ${path.relative(projectRoot, f)}. Use force: true to delete anyway.`);
970
- }
971
- }
972
- catch { }
973
- }
974
- }
975
- }
976
- const result = removeSymbolFromFile(resolvedPath, fileContent, symbolName, className);
977
- if (!result.success) {
978
- return errorResponse(result.error || "Unknown error during symbol removal");
979
- }
980
- const diff = generateDiff(fileContent, result.newContent, resolvedPath);
981
- const token = confirmationCache.add("remove_symbol", resolvedPath, {
982
- symbolName,
983
- newContentFull: result.newContent,
984
- diff
985
- });
986
- return {
987
- content: [{
988
- type: "text",
989
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${diff}`,
990
- }],
991
- };
992
- }
993
- // ─── Import Resolution Logic ────────────────────────────────────────
994
- function resolveImportMatch(importPath, targetFile, sourceFile, rootDir) {
995
- const normalized = importPath.replace(/\\/g, "/");
996
- const targetResolved = path.resolve(targetFile);
997
- const targetWithoutExt = targetResolved.replace(/\.[^.]+$/, "");
998
- const targetBaseName = path.basename(targetFile, path.extname(targetFile));
999
- // Relative imports (./foo, ../bar)
1000
- if (normalized.startsWith(".")) {
1001
- const sourceDir = path.dirname(sourceFile);
1002
- const resolved = path.resolve(sourceDir, normalized);
1003
- // Try exact match, then with common extensions, then /index variants
1004
- const candidates = [
1005
- resolved,
1006
- resolved + ".ts",
1007
- resolved + ".tsx",
1008
- resolved + ".js",
1009
- resolved + ".jsx",
1010
- resolved + ".mts",
1011
- resolved + ".mjs",
1012
- resolved + ".dart",
1013
- path.join(resolved, "index.ts"),
1014
- path.join(resolved, "index.tsx"),
1015
- path.join(resolved, "index.js"),
1016
- path.join(resolved, "index.mjs"),
1017
- ];
1018
- for (const candidate of candidates) {
1019
- if (path.resolve(candidate) === targetResolved ||
1020
- path.resolve(candidate) === targetWithoutExt) {
1021
- return true;
1022
- }
1023
- }
1024
- return false;
1025
- }
1026
- // Non-relative imports (aliases, packages)
1027
- // Check if the import path ends with the target's relative path or basename
1028
- const targetRelative = path
1029
- .relative(rootDir, targetFile)
1030
- .replace(/\\/g, "/")
1031
- .replace(/\.[^.]+$/, ""); // strip extension
1032
- if (normalized === targetBaseName ||
1033
- normalized.endsWith("/" + targetBaseName) ||
1034
- normalized === targetRelative ||
1035
- normalized.endsWith(targetRelative)) {
1036
- return true;
1037
- }
1038
- // Python-style dotted imports: convert dots to path separators
1039
- const dottedPath = normalized.replace(/\./g, "/");
1040
- if (dottedPath === targetBaseName ||
1041
- dottedPath.endsWith("/" + targetBaseName) ||
1042
- dottedPath === targetRelative ||
1043
- dottedPath.endsWith(targetRelative)) {
1044
- return true;
1045
- }
1046
- return false;
1047
- }
1048
- // ─── Project Root Discovery ─────────────────────────────────────────
1049
- function findProjectRoot(filePath) {
1050
- const ROOT_MARKERS = [
1051
- "package.json",
1052
- ".git",
1053
- "pyproject.toml",
1054
- "setup.py",
1055
- "Cargo.toml",
1056
- "go.mod",
1057
- "pom.xml",
1058
- "build.gradle",
1059
- "CMakeLists.txt",
1060
- ".gitignore",
1061
- "Gemfile",
1062
- "composer.json",
1063
- "pubspec.yaml",
1064
- ];
1065
- let current = path.dirname(path.resolve(filePath));
1066
- const root = path.parse(current).root;
1067
- while (current !== root) {
1068
- for (const marker of ROOT_MARKERS) {
1069
- if (fs.existsSync(path.join(current, marker))) {
1070
- return current;
1071
- }
1072
- }
1073
- current = path.dirname(current);
1074
- }
1075
- return undefined;
1076
- }
1077
- function formatAsXml(projectName, projectPath, entries, skippedCount) {
1078
- const lines = [
1079
- `<?xml version="1.0" encoding="UTF-8"?>`,
1080
- `<repository name="${esc(projectName)}" path="${esc(projectPath)}" files="${entries.length}" skipped="${skippedCount}">`,
1081
- ``,
1082
- ` <file_tree>`,
1083
- ];
1084
- for (const entry of entries) {
1085
- lines.push(` <file path="${esc(entry.relativePath)}" language="${esc(entry.language)}" />`);
1086
- }
1087
- lines.push(` </file_tree>`);
1088
- lines.push(``);
1089
- for (const entry of entries) {
1090
- lines.push(` <file path="${esc(entry.relativePath)}" language="${esc(entry.language)}">`);
1091
- lines.push(`<compressed>`);
1092
- lines.push(entry.compressed);
1093
- lines.push(`</compressed>`);
1094
- lines.push(` </file>`);
1095
- lines.push(``);
1096
- }
1097
- lines.push(`</repository>`);
1098
- return lines.join("\n");
1099
- }
1100
- function formatAsMarkdown(projectName, projectPath, entries, skippedCount) {
1101
- const lines = [
1102
- `# Repository Map: ${projectName}`,
1103
- ``,
1104
- `> **Path:** \`${projectPath}\` `,
1105
- `> **Source files:** ${entries.length} | **Skipped:** ${skippedCount}`,
1106
- ``,
1107
- `## File Tree`,
1108
- ``,
1109
- ];
1110
- for (const entry of entries) {
1111
- lines.push(`- \`${entry.relativePath}\` *(${entry.language})*`);
1112
- }
1113
- lines.push(``);
1114
- lines.push(`---`);
1115
- lines.push(``);
1116
- for (const entry of entries) {
1117
- const langId = getLangId(entry.language);
1118
- lines.push(`### ${entry.relativePath}`);
1119
- lines.push("```" + langId);
1120
- lines.push(entry.compressed);
1121
- lines.push("```");
1122
- lines.push(``);
1123
- }
1124
- return lines.join("\n");
1125
- }
1126
- function buildImpactReport(targetRelative, rootDir, dependents) {
1127
- const lines = [
1128
- `# Impact Analysis`,
1129
- ``,
1130
- `## Target`,
1131
- `- **File:** \`${targetRelative}\``,
1132
- `- **Project root:** \`${rootDir}\``,
1133
- `- **Dependent files:** ${dependents.length}`,
1134
- ``,
1135
- ];
1136
- if (dependents.length === 0) {
1137
- lines.push(`> No files in this repository import or depend on the target file.`);
1138
- lines.push(``);
1139
- lines.push(`This could mean the file is a leaf module, an entry point, ` +
1140
- `or uses non-standard import patterns not detected by this analysis.`);
1141
- }
1142
- else {
1143
- lines.push(`## Dependent Files`);
1144
- lines.push(``);
1145
- for (const dep of dependents) {
1146
- lines.push(`### \`${dep.file}\``);
1147
- for (const imp of dep.imports) {
1148
- lines.push(`- \`${imp}\``);
1149
- }
1150
- lines.push(``);
1151
- }
1152
- lines.push(`---`);
1153
- lines.push(``);
1154
- lines.push(`> **⚠️ Blast radius:** Modifying \`${targetRelative}\` may affect ` +
1155
- `**${dependents.length}** file(s). Review the import statements above ` +
1156
- `to understand what each dependent file consumes from the target.`);
1157
- }
1158
- return lines.join("\n");
1159
- }
1160
451
  // ─── Utility ────────────────────────────────────────────────────────
1161
- function getLanguageName(ext) {
1162
- const map = {
1163
- ".ts": "TypeScript",
1164
- ".tsx": "TypeScript JSX",
1165
- ".mts": "TypeScript",
1166
- ".cts": "TypeScript",
1167
- ".js": "JavaScript",
1168
- ".jsx": "JavaScript JSX",
1169
- ".mjs": "JavaScript",
1170
- ".cjs": "JavaScript",
1171
- ".py": "Python",
1172
- ".pyi": "Python Stub",
1173
- ".json": "JSON",
1174
- ".yaml": "YAML",
1175
- ".yml": "YAML",
1176
- ".toml": "TOML",
1177
- ".md": "Markdown",
1178
- ".txt": "Text",
1179
- ".css": "CSS",
1180
- ".scss": "SCSS",
1181
- ".less": "LESS",
1182
- ".html": "HTML",
1183
- ".vue": "Vue",
1184
- ".svelte": "Svelte",
1185
- ".rs": "Rust",
1186
- ".go": "Go",
1187
- ".java": "Java",
1188
- ".kt": "Kotlin",
1189
- ".c": "C",
1190
- ".cpp": "C++",
1191
- ".h": "C Header",
1192
- ".hpp": "C++ Header",
1193
- ".rb": "Ruby",
1194
- ".php": "PHP",
1195
- ".dart": "Dart",
1196
- ".swift": "Swift",
1197
- ".sh": "Shell",
1198
- ".bash": "Bash",
1199
- ".zsh": "Zsh",
1200
- ".sql": "SQL",
1201
- ".graphql": "GraphQL",
1202
- ".gql": "GraphQL",
1203
- ".proto": "Protocol Buffers",
1204
- ".dockerfile": "Dockerfile",
1205
- };
1206
- return map[ext] || ext.substring(1).toUpperCase();
1207
- }
1208
- function getLangId(language) {
1209
- const map = {
1210
- TypeScript: "typescript",
1211
- "TypeScript JSX": "tsx",
1212
- JavaScript: "javascript",
1213
- "JavaScript JSX": "jsx",
1214
- Python: "python",
1215
- "Python Stub": "python",
1216
- JSON: "json",
1217
- YAML: "yaml",
1218
- TOML: "toml",
1219
- Markdown: "markdown",
1220
- CSS: "css",
1221
- SCSS: "scss",
1222
- LESS: "less",
1223
- HTML: "html",
1224
- Vue: "vue",
1225
- Svelte: "svelte",
1226
- Rust: "rust",
1227
- Go: "go",
1228
- Java: "java",
1229
- Kotlin: "kotlin",
1230
- C: "c",
1231
- "C++": "cpp",
1232
- Ruby: "ruby",
1233
- PHP: "php",
1234
- Dart: "dart",
1235
- Swift: "swift",
1236
- Shell: "bash",
1237
- SQL: "sql",
1238
- GraphQL: "graphql",
1239
- };
1240
- return map[language] || language.toLowerCase();
1241
- }
1242
- function esc(str) {
1243
- return str
1244
- .replace(/&/g, "&amp;")
1245
- .replace(/</g, "&lt;")
1246
- .replace(/>/g, "&gt;")
1247
- .replace(/"/g, "&quot;")
1248
- .replace(/'/g, "&apos;");
1249
- }
1250
- export async function handleRollbackFile(args) {
1251
- const filePath = args.filePath;
1252
- const steps = args.steps || 1;
1253
- if (!filePath)
1254
- return errorResponse("Missing required parameter: filePath");
1255
- const resolvedPath = path.resolve(filePath);
1256
- if (steps < 1 || steps > 5) {
1257
- return errorResponse("Steps must be between 1 and 5.");
1258
- }
1259
- const success = restoreBackup(resolvedPath, steps);
1260
- if (!success) {
1261
- return errorResponse(`No backup found for step ${steps} at ${resolvedPath}`);
1262
- }
1263
- return {
1264
- content: [{
1265
- type: "text",
1266
- text: `// ⏪ Rollback successful!\n// Reverted ${path.basename(resolvedPath)} to state from ${steps} step(s) ago.\n// File: ${resolvedPath}`,
1267
- }],
1268
- };
1269
- }
1270
452
  function errorResponse(message) {
1271
453
  return {
1272
454
  content: [{ type: "text", text: `❌ ${message}` }],