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
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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,45 +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
|
-
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { extractFallbackSymbols, findClosestSymbols } from "./utils/fuzzyMatch.js";
|
|
31
|
-
import { confirmationCache } from "./utils/confirmationCache.js";
|
|
32
|
-
import { readFileLines } from "./tools/readFileLines.js";
|
|
33
|
-
import { searchCodePattern } from "./tools/searchCodePattern.js";
|
|
34
|
-
import * as fs from "node:fs";
|
|
35
|
-
import * as path from "node:path";
|
|
36
|
-
// ─── Constants ──────────────────────────────────────────────────────
|
|
37
|
-
const SERVER_NAME = "mcp-code-context";
|
|
38
|
-
const SERVER_VERSION = "2.2.0";
|
|
39
|
-
/** Extensions we consider source code for analysis */
|
|
40
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
41
|
-
".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
|
|
42
|
-
".py", ".pyi",
|
|
43
|
-
".json", ".yaml", ".yml", ".toml",
|
|
44
|
-
".md", ".txt",
|
|
45
|
-
".css", ".scss", ".less",
|
|
46
|
-
".html", ".vue", ".svelte",
|
|
47
|
-
".rs", ".go", ".java", ".kt", ".dart",
|
|
48
|
-
".c", ".cpp", ".h", ".hpp",
|
|
49
|
-
".rb", ".php", ".swift",
|
|
50
|
-
".sh", ".bash", ".zsh",
|
|
51
|
-
".sql",
|
|
52
|
-
".graphql", ".gql",
|
|
53
|
-
".proto",
|
|
54
|
-
".dockerfile",
|
|
55
|
-
]);
|
|
56
|
-
/** Extensions eligible for import analysis */
|
|
57
|
-
const IMPORTABLE_EXTENSIONS = new Set([
|
|
58
|
-
".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
|
|
59
|
-
".py", ".pyi",
|
|
60
|
-
".php", ".phtml",
|
|
61
|
-
".dart",
|
|
62
|
-
".vue", ".svelte",
|
|
63
|
-
]);
|
|
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";
|
|
64
24
|
// ─── MCP Server Instance ────────────────────────────────────────────
|
|
65
25
|
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
66
26
|
// ─── Tool Definitions ───────────────────────────────────────────────
|
|
@@ -121,38 +81,28 @@ const TOOLS = [
|
|
|
121
81
|
},
|
|
122
82
|
},
|
|
123
83
|
{
|
|
124
|
-
name: "
|
|
125
|
-
description: "
|
|
84
|
+
name: "analyze_impact",
|
|
85
|
+
description: "Analyzes the impact of modifying a given file by discovering all other " +
|
|
86
|
+
"files in the repository that import or depend on it. Scans for ES import/export, " +
|
|
87
|
+
"CommonJS require(), Python import, PHP use/require_once/include, and Dart import statements. " +
|
|
88
|
+
"Returns a structured report listing all dependent files and the specific " +
|
|
89
|
+
"import statements that create the dependency. Essential for understanding " +
|
|
90
|
+
"blast radius before refactoring.",
|
|
126
91
|
inputSchema: {
|
|
127
92
|
type: "object",
|
|
128
93
|
properties: {
|
|
129
94
|
filePath: {
|
|
130
95
|
type: "string",
|
|
131
|
-
description: "Absolute path to the
|
|
96
|
+
description: "Absolute path to the file being modified (the dependency target).",
|
|
132
97
|
},
|
|
133
|
-
|
|
134
|
-
type: "number",
|
|
135
|
-
description: "Number of steps back to revert to (1-5). Defaults to 1 (most recent backup).",
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
required: ["filePath"],
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
name: "clean_backups",
|
|
143
|
-
description: "Removes all backup files for a project. Deletes the entire .mcp-backups directory " +
|
|
144
|
-
"at the project root, freeing up disk space and cleaning the working directory. " +
|
|
145
|
-
"Use this to clean up after completing a series of edits or when backups are no longer needed.",
|
|
146
|
-
inputSchema: {
|
|
147
|
-
type: "object",
|
|
148
|
-
properties: {
|
|
149
|
-
projectRoot: {
|
|
98
|
+
rootDir: {
|
|
150
99
|
type: "string",
|
|
151
|
-
description: "Absolute path to the
|
|
152
|
-
"
|
|
100
|
+
description: "Optional. Absolute path to the repository root. If omitted, " +
|
|
101
|
+
"the server walks up from filePath to find the nearest directory " +
|
|
102
|
+
"containing package.json, .git, or similar root markers.",
|
|
153
103
|
},
|
|
154
104
|
},
|
|
155
|
-
required: ["
|
|
105
|
+
required: ["filePath"],
|
|
156
106
|
},
|
|
157
107
|
},
|
|
158
108
|
{
|
|
@@ -236,32 +186,6 @@ const TOOLS = [
|
|
|
236
186
|
required: ["rootDir", "pattern"],
|
|
237
187
|
},
|
|
238
188
|
},
|
|
239
|
-
{
|
|
240
|
-
name: "analyze_impact",
|
|
241
|
-
description: "Analyzes the impact of modifying a given file by discovering all other " +
|
|
242
|
-
"files in the repository that import or depend on it. Scans for ES import/export, " +
|
|
243
|
-
"CommonJS require(), Python import, PHP use/require_once/include, and Dart import statements. " +
|
|
244
|
-
"Returns a structured report listing all dependent files and the specific " +
|
|
245
|
-
"import statements that create the dependency. Essential for understanding " +
|
|
246
|
-
"blast radius before refactoring.",
|
|
247
|
-
inputSchema: {
|
|
248
|
-
type: "object",
|
|
249
|
-
properties: {
|
|
250
|
-
filePath: {
|
|
251
|
-
type: "string",
|
|
252
|
-
description: "Absolute path to the file being modified (the dependency target).",
|
|
253
|
-
},
|
|
254
|
-
rootDir: {
|
|
255
|
-
type: "string",
|
|
256
|
-
description: "Optional. Absolute path to the repository root. If omitted, " +
|
|
257
|
-
"the server walks up from filePath to find the nearest directory " +
|
|
258
|
-
"containing package.json, .git, or similar root markers.",
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
required: ["filePath"],
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
// ─── Write Tools ────────────────────────────────────────────────────
|
|
265
189
|
{
|
|
266
190
|
name: "write_file_surgical",
|
|
267
191
|
description: "Surgically replaces the full source code of a named symbol (function, class, " +
|
|
@@ -343,9 +267,13 @@ const TOOLS = [
|
|
|
343
267
|
description: "Optional. Name of the class to scope the anchor symbol search to. " +
|
|
344
268
|
"Use when multiple classes have methods with the same name.",
|
|
345
269
|
},
|
|
346
|
-
|
|
270
|
+
confirmationToken: {
|
|
271
|
+
type: "string",
|
|
272
|
+
description: "Required for Phase 2. The token provided from the Phase 1 Dry-Run preview.",
|
|
273
|
+
},
|
|
274
|
+
confirm: {
|
|
347
275
|
type: "boolean",
|
|
348
|
-
description: "
|
|
276
|
+
description: "Required for Phase 2. Set to true to confirm and apply the changes.",
|
|
349
277
|
},
|
|
350
278
|
createBackup: {
|
|
351
279
|
type: "boolean",
|
|
@@ -426,9 +354,13 @@ const TOOLS = [
|
|
|
426
354
|
description: "If true, skip the dependency check and remove the symbol even if " +
|
|
427
355
|
"other files reference it. Defaults to false.",
|
|
428
356
|
},
|
|
429
|
-
|
|
357
|
+
confirmationToken: {
|
|
358
|
+
type: "string",
|
|
359
|
+
description: "Required for Phase 2. The token provided from the Phase 1 Dry-Run preview.",
|
|
360
|
+
},
|
|
361
|
+
confirm: {
|
|
430
362
|
type: "boolean",
|
|
431
|
-
description: "
|
|
363
|
+
description: "Required for Phase 2. Set to true to confirm and apply the changes.",
|
|
432
364
|
},
|
|
433
365
|
createBackup: {
|
|
434
366
|
type: "boolean",
|
|
@@ -438,6 +370,41 @@ const TOOLS = [
|
|
|
438
370
|
required: ["filePath", "symbolName"],
|
|
439
371
|
},
|
|
440
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
|
+
},
|
|
441
408
|
];
|
|
442
409
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
443
410
|
tools: TOOLS,
|
|
@@ -447,12 +414,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
447
414
|
const { name, arguments: args } = request.params;
|
|
448
415
|
try {
|
|
449
416
|
switch (name) {
|
|
417
|
+
// Read tools
|
|
450
418
|
case "get_semantic_repo_map":
|
|
451
419
|
return await handleGetSemanticRepoMap(args);
|
|
452
420
|
case "read_file_surgical":
|
|
453
421
|
return await handleReadFileSurgical(args);
|
|
454
422
|
case "analyze_impact":
|
|
455
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
|
|
456
429
|
case "write_file_surgical":
|
|
457
430
|
return await handleWriteFileSurgical(args);
|
|
458
431
|
case "insert_symbol":
|
|
@@ -461,14 +434,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
461
434
|
return await handleRenameSymbol(args);
|
|
462
435
|
case "remove_symbol":
|
|
463
436
|
return await handleRemoveSymbol(args);
|
|
437
|
+
// Util tools
|
|
464
438
|
case "rollback_file":
|
|
465
439
|
return await handleRollbackFile(args);
|
|
466
440
|
case "clean_backups":
|
|
467
441
|
return await handleCleanBackups(args);
|
|
468
|
-
case "read_file_lines":
|
|
469
|
-
return await handleReadFileLines(args);
|
|
470
|
-
case "search_code_pattern":
|
|
471
|
-
return await handleSearchCodePattern(args);
|
|
472
442
|
default:
|
|
473
443
|
return errorResponse(`Unknown tool: "${name}"`);
|
|
474
444
|
}
|
|
@@ -478,991 +448,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
478
448
|
return errorResponse(`Unhandled error in tool "${name}": ${message}`);
|
|
479
449
|
}
|
|
480
450
|
});
|
|
481
|
-
// ─── Tool 1: get_semantic_repo_map ──────────────────────────────────
|
|
482
|
-
export async function handleGetSemanticRepoMap(args) {
|
|
483
|
-
const directoryPath = args.directoryPath;
|
|
484
|
-
const format = args.format || "xml";
|
|
485
|
-
if (!directoryPath) {
|
|
486
|
-
return errorResponse("Missing required parameter: directoryPath");
|
|
487
|
-
}
|
|
488
|
-
const resolvedPath = path.resolve(directoryPath);
|
|
489
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
490
|
-
return errorResponse(`Directory not found: ${resolvedPath}`);
|
|
491
|
-
}
|
|
492
|
-
const stat = fs.statSync(resolvedPath);
|
|
493
|
-
if (!stat.isDirectory()) {
|
|
494
|
-
return errorResponse(`Path is not a directory: ${resolvedPath}`);
|
|
495
|
-
}
|
|
496
|
-
const ignoreManager = new IgnoreManager(resolvedPath);
|
|
497
|
-
const files = ignoreManager.walkDirectory();
|
|
498
|
-
const projectName = path.basename(resolvedPath);
|
|
499
|
-
const entries = [];
|
|
500
|
-
let skippedCount = 0;
|
|
501
|
-
for (const file of files) {
|
|
502
|
-
const ext = path.extname(file).toLowerCase();
|
|
503
|
-
if (!SOURCE_EXTENSIONS.has(ext)) {
|
|
504
|
-
skippedCount++;
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
let content;
|
|
508
|
-
try {
|
|
509
|
-
content = fs.readFileSync(file, "utf-8");
|
|
510
|
-
}
|
|
511
|
-
catch {
|
|
512
|
-
skippedCount++;
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
// Skip files with null bytes (binary masquerading as text)
|
|
516
|
-
if (content.includes("\0")) {
|
|
517
|
-
skippedCount++;
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
const relativePath = path
|
|
521
|
-
.relative(resolvedPath, file)
|
|
522
|
-
.replace(/\\/g, "/");
|
|
523
|
-
const language = getLanguageName(ext);
|
|
524
|
-
const compressed = compressFile(file, content);
|
|
525
|
-
entries.push({ relativePath, language, compressed });
|
|
526
|
-
}
|
|
527
|
-
const output = format === "markdown"
|
|
528
|
-
? formatAsMarkdown(projectName, resolvedPath, entries, skippedCount)
|
|
529
|
-
: formatAsXml(projectName, resolvedPath, entries, skippedCount);
|
|
530
|
-
return {
|
|
531
|
-
content: [{ type: "text", text: output }],
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
// ─── Tool 2: read_file_surgical ─────────────────────────────────────
|
|
535
|
-
export async function handleReadFileSurgical(args) {
|
|
536
|
-
const filePath = args.filePath;
|
|
537
|
-
const symbolName = args.symbolName;
|
|
538
|
-
const className = args.className;
|
|
539
|
-
if (!filePath) {
|
|
540
|
-
return errorResponse("Missing required parameter: filePath");
|
|
541
|
-
}
|
|
542
|
-
const resolvedPath = path.resolve(filePath);
|
|
543
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
544
|
-
return errorResponse(`File not found: ${resolvedPath}`);
|
|
545
|
-
}
|
|
546
|
-
let content;
|
|
547
|
-
try {
|
|
548
|
-
content = fs.readFileSync(resolvedPath, "utf-8");
|
|
549
|
-
}
|
|
550
|
-
catch (error) {
|
|
551
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
552
|
-
return errorResponse(`Failed to read file: ${msg}`);
|
|
553
|
-
}
|
|
554
|
-
// If no symbol requested, return the full file
|
|
555
|
-
if (!symbolName) {
|
|
556
|
-
return {
|
|
557
|
-
content: [
|
|
558
|
-
{
|
|
559
|
-
type: "text",
|
|
560
|
-
text: `// File: ${resolvedPath}\n// Lines: ${content.split("\n").length}\n\n${content}`,
|
|
561
|
-
},
|
|
562
|
-
],
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
// Attempt surgical extraction
|
|
566
|
-
const extracted = extractSymbol(content, symbolName, resolvedPath, className);
|
|
567
|
-
if (extracted === null) {
|
|
568
|
-
// Symbol not found — use fuzzy matching to return suggestions
|
|
569
|
-
const available = extractFallbackSymbols(content);
|
|
570
|
-
const closest = findClosestSymbols(symbolName, available, 5);
|
|
571
|
-
const availableFormatted = available.slice(0, 15).map(s => `${s.name} (${s.type}${s.className ? ` in ${s.className}` : ''})`);
|
|
572
|
-
if (available.length > 15)
|
|
573
|
-
availableFormatted.push(`...and ${available.length - 15} more`);
|
|
574
|
-
const suggestions = [];
|
|
575
|
-
if (className) {
|
|
576
|
-
// Check if it exists in another class
|
|
577
|
-
const inOtherClass = available.find(s => s.name === symbolName && s.className !== className);
|
|
578
|
-
if (inOtherClass)
|
|
579
|
-
suggestions.push(`Did you mean '${symbolName}' in class '${inOtherClass.className}'? Update your className parameter.`);
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
// Check if it requires a class scope
|
|
583
|
-
const needsScope = available.filter(s => s.name === symbolName && s.className);
|
|
584
|
-
if (needsScope.length > 0)
|
|
585
|
-
suggestions.push(`Symbol '${symbolName}' exists inside multiple scopes. Did you mean to use className: '${needsScope[0].className}'?`);
|
|
586
|
-
}
|
|
587
|
-
if (closest.length > 0) {
|
|
588
|
-
suggestions.push(`Similar symbols: ${closest.map(s => s.name).join(', ')}`);
|
|
589
|
-
}
|
|
590
|
-
const errorPayload = {
|
|
591
|
-
error: `Symbol '${symbolName}' not found in ${path.basename(resolvedPath)}`,
|
|
592
|
-
available_symbols: availableFormatted,
|
|
593
|
-
suggestions
|
|
594
|
-
};
|
|
595
|
-
return {
|
|
596
|
-
isError: true,
|
|
597
|
-
content: [
|
|
598
|
-
{
|
|
599
|
-
type: "text",
|
|
600
|
-
text: JSON.stringify(errorPayload, null, 2),
|
|
601
|
-
},
|
|
602
|
-
],
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
return {
|
|
606
|
-
content: [
|
|
607
|
-
{
|
|
608
|
-
type: "text",
|
|
609
|
-
text: `// Symbol: ${symbolName}\n` +
|
|
610
|
-
`// File: ${resolvedPath}\n\n${extracted}`,
|
|
611
|
-
},
|
|
612
|
-
],
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
// ─── Tool 3: analyze_impact ─────────────────────────────────────────
|
|
616
|
-
export async function handleAnalyzeImpact(args) {
|
|
617
|
-
const filePath = args.filePath;
|
|
618
|
-
let rootDir = args.rootDir;
|
|
619
|
-
if (!filePath) {
|
|
620
|
-
return errorResponse("Missing required parameter: filePath");
|
|
621
|
-
}
|
|
622
|
-
const resolvedPath = path.resolve(filePath);
|
|
623
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
624
|
-
return errorResponse(`File not found: ${resolvedPath}`);
|
|
625
|
-
}
|
|
626
|
-
// Discover project root if not provided
|
|
627
|
-
if (!rootDir) {
|
|
628
|
-
rootDir = findProjectRoot(resolvedPath);
|
|
629
|
-
}
|
|
630
|
-
if (!rootDir || !fs.existsSync(rootDir)) {
|
|
631
|
-
return errorResponse(`Could not determine project root for ${resolvedPath}. ` +
|
|
632
|
-
`Provide rootDir explicitly.`);
|
|
633
|
-
}
|
|
634
|
-
rootDir = path.resolve(rootDir);
|
|
635
|
-
const ignoreManager = new IgnoreManager(rootDir);
|
|
636
|
-
const allFiles = ignoreManager.walkDirectory();
|
|
637
|
-
const targetRelative = path
|
|
638
|
-
.relative(rootDir, resolvedPath)
|
|
639
|
-
.replace(/\\/g, "/");
|
|
640
|
-
// Import pattern regexes
|
|
641
|
-
const importPatterns = [
|
|
642
|
-
// ES static import: import ... from "path"
|
|
643
|
-
/import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/gm,
|
|
644
|
-
// ES dynamic import: import("path")
|
|
645
|
-
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
|
|
646
|
-
// CommonJS: require("path")
|
|
647
|
-
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
|
|
648
|
-
// Python: from package import ...
|
|
649
|
-
/from\s+(\S+)\s+import/gm,
|
|
650
|
-
// Python: import package
|
|
651
|
-
/^import\s+(\S+)/gm,
|
|
652
|
-
// PHP: use Namespace\Class
|
|
653
|
-
/^\s*use\s+([\w\\]+)/gm,
|
|
654
|
-
// PHP: require_once / require / include_once / include
|
|
655
|
-
/(?:require_once|require|include_once|include)\s*\(?\s*['"]([^'"]+)['"]\s*\)?/gm,
|
|
656
|
-
// Dart: import 'package:...' or import '../...';
|
|
657
|
-
/^\s*import\s+['"]([^'"]+)['"]\s*;/gm,
|
|
658
|
-
];
|
|
659
|
-
const dependents = [];
|
|
660
|
-
for (const file of allFiles) {
|
|
661
|
-
// Skip the target file itself
|
|
662
|
-
if (path.resolve(file) === resolvedPath)
|
|
663
|
-
continue;
|
|
664
|
-
// Only scan importable file types
|
|
665
|
-
const ext = path.extname(file).toLowerCase();
|
|
666
|
-
if (!IMPORTABLE_EXTENSIONS.has(ext))
|
|
667
|
-
continue;
|
|
668
|
-
let content;
|
|
669
|
-
try {
|
|
670
|
-
content = fs.readFileSync(file, "utf-8");
|
|
671
|
-
}
|
|
672
|
-
catch {
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
let isDependent = false;
|
|
676
|
-
const matchedImports = [];
|
|
677
|
-
for (const pattern of importPatterns) {
|
|
678
|
-
const regex = new RegExp(pattern.source, pattern.flags);
|
|
679
|
-
let match;
|
|
680
|
-
while ((match = regex.exec(content)) !== null) {
|
|
681
|
-
const importPath = match[1];
|
|
682
|
-
if (resolveImportMatch(importPath, resolvedPath, file, rootDir)) {
|
|
683
|
-
matchedImports.push(match[0].trim());
|
|
684
|
-
isDependent = true;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (isDependent) {
|
|
689
|
-
dependents.push({
|
|
690
|
-
file: path.relative(rootDir, file).replace(/\\/g, "/"),
|
|
691
|
-
imports: [...new Set(matchedImports)], // deduplicate
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
// Build the report
|
|
696
|
-
const report = buildImpactReport(targetRelative, rootDir, dependents);
|
|
697
|
-
return {
|
|
698
|
-
content: [{ type: "text", text: report }],
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
// ─── Tool 4: write_file_surgical ────────────────────────────────────
|
|
702
|
-
export async function handleWriteFileSurgical(args) {
|
|
703
|
-
const filePath = args.filePath;
|
|
704
|
-
const symbolName = args.symbolName;
|
|
705
|
-
const newContent = args.newContent;
|
|
706
|
-
const className = args.className;
|
|
707
|
-
const shouldBackup = args.createBackup || false;
|
|
708
|
-
const confirmationToken = args.confirmationToken;
|
|
709
|
-
const confirm = args.confirm;
|
|
710
|
-
if (!filePath)
|
|
711
|
-
return errorResponse("Missing required parameter: filePath");
|
|
712
|
-
// PHASE 2: Apply the change
|
|
713
|
-
if (confirmationToken && confirm) {
|
|
714
|
-
const pending = confirmationCache.consume(confirmationToken);
|
|
715
|
-
if (!pending || pending.operation !== "write_file_surgical") {
|
|
716
|
-
return errorResponse("Invalid or expired confirmation token. Please run the operation again to generate a new preview.");
|
|
717
|
-
}
|
|
718
|
-
const resolvedPath = pending.filePath;
|
|
719
|
-
try {
|
|
720
|
-
createBackup(resolvedPath);
|
|
721
|
-
}
|
|
722
|
-
catch (error) {
|
|
723
|
-
return errorResponse(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`);
|
|
724
|
-
}
|
|
725
|
-
try {
|
|
726
|
-
fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
|
|
727
|
-
}
|
|
728
|
-
catch (error) {
|
|
729
|
-
restoreBackup(resolvedPath);
|
|
730
|
-
return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
|
|
731
|
-
}
|
|
732
|
-
return {
|
|
733
|
-
content: [
|
|
734
|
-
{
|
|
735
|
-
type: "text",
|
|
736
|
-
text: `// ✅ Symbol "${pending.payload.symbolName}" replaced successfully\n// File: ${resolvedPath}\n\n${pending.payload.diff}`,
|
|
737
|
-
},
|
|
738
|
-
],
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
// PHASE 1: Dry-Run Preview
|
|
742
|
-
if (!symbolName)
|
|
743
|
-
return errorResponse("Missing required parameter: symbolName");
|
|
744
|
-
if (!newContent)
|
|
745
|
-
return errorResponse("Missing required parameter: newContent");
|
|
746
|
-
const resolvedPath = path.resolve(filePath);
|
|
747
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
748
|
-
return errorResponse(`File not found: ${resolvedPath}`);
|
|
749
|
-
}
|
|
750
|
-
const ext = path.extname(resolvedPath).toLowerCase();
|
|
751
|
-
if (!WRITABLE_EXTENSIONS.has(ext)) {
|
|
752
|
-
return errorResponse(`Unsupported file type "${ext}" for surgical writing. ` +
|
|
753
|
-
`Supported: .ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs, .php, .phtml, .dart, .py, .pyi`);
|
|
754
|
-
}
|
|
755
|
-
let content;
|
|
756
|
-
try {
|
|
757
|
-
content = fs.readFileSync(resolvedPath, "utf-8");
|
|
758
|
-
}
|
|
759
|
-
catch (error) {
|
|
760
|
-
return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
|
|
761
|
-
}
|
|
762
|
-
// Perform the replacement memory-only
|
|
763
|
-
const result = replaceSymbol(resolvedPath, content, symbolName, newContent, className);
|
|
764
|
-
if (!result.success) {
|
|
765
|
-
const isNotFoundError = result.error && (result.error.includes("not found") || result.error.includes("Could not locate"));
|
|
766
|
-
if (isNotFoundError) {
|
|
767
|
-
const available = extractFallbackSymbols(content);
|
|
768
|
-
const closest = findClosestSymbols(symbolName, available, 5);
|
|
769
|
-
const availableFormatted = available.slice(0, 15).map(s => `${s.name} (${s.type}${s.className ? ` in ${s.className}` : ''})`);
|
|
770
|
-
if (available.length > 15)
|
|
771
|
-
availableFormatted.push(`...and ${available.length - 15} more`);
|
|
772
|
-
const suggestions = [];
|
|
773
|
-
if (className) {
|
|
774
|
-
const inOtherClass = available.find(s => s.name === symbolName && s.className !== className);
|
|
775
|
-
if (inOtherClass)
|
|
776
|
-
suggestions.push(`Did you mean '${symbolName}' in class '${inOtherClass.className}'? Update your className parameter.`);
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
const needsScope = available.filter(s => s.name === symbolName && s.className);
|
|
780
|
-
if (needsScope.length > 0)
|
|
781
|
-
suggestions.push(`Symbol '${symbolName}' exists inside multiple scopes. Did you mean to use className: '${needsScope[0].className}'?`);
|
|
782
|
-
}
|
|
783
|
-
if (closest.length > 0) {
|
|
784
|
-
suggestions.push(`Similar symbols: ${closest.map(s => s.name).join(', ')}`);
|
|
785
|
-
}
|
|
786
|
-
return {
|
|
787
|
-
isError: true,
|
|
788
|
-
content: [
|
|
789
|
-
{
|
|
790
|
-
type: "text",
|
|
791
|
-
text: JSON.stringify({
|
|
792
|
-
error: result.error,
|
|
793
|
-
available_symbols: availableFormatted,
|
|
794
|
-
suggestions
|
|
795
|
-
}, null, 2),
|
|
796
|
-
},
|
|
797
|
-
],
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
return errorResponse(result.error || "Unknown error during symbol replacement");
|
|
801
|
-
}
|
|
802
|
-
const diff = generateDiff(content, result.newContent, resolvedPath);
|
|
803
|
-
const token = confirmationCache.add("write_file_surgical", resolvedPath, {
|
|
804
|
-
symbolName,
|
|
805
|
-
newContentFull: result.newContent,
|
|
806
|
-
diff,
|
|
807
|
-
});
|
|
808
|
-
return {
|
|
809
|
-
content: [
|
|
810
|
-
{
|
|
811
|
-
type: "text",
|
|
812
|
-
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}`,
|
|
813
|
-
},
|
|
814
|
-
],
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
// ─── Tool 5: insert_symbol ──────────────────────────────────────────
|
|
818
|
-
export async function handleInsertSymbol(args) {
|
|
819
|
-
const filePath = args.filePath;
|
|
820
|
-
const code = args.code;
|
|
821
|
-
const anchorSymbol = args.anchorSymbol || null;
|
|
822
|
-
const position = args.position || "after";
|
|
823
|
-
const className = args.className;
|
|
824
|
-
const shouldBackup = args.createBackup || false;
|
|
825
|
-
const confirmationToken = args.confirmationToken;
|
|
826
|
-
const confirm = args.confirm;
|
|
827
|
-
if (!filePath)
|
|
828
|
-
return errorResponse("Missing required parameter: filePath");
|
|
829
|
-
// PHASE 2: Apply the change
|
|
830
|
-
if (confirmationToken && confirm) {
|
|
831
|
-
const pending = confirmationCache.consume(confirmationToken);
|
|
832
|
-
if (!pending || pending.operation !== "insert_symbol") {
|
|
833
|
-
return errorResponse("Invalid or expired confirmation token. Please run the operation again to generate a new preview.");
|
|
834
|
-
}
|
|
835
|
-
const resolvedPath = pending.filePath;
|
|
836
|
-
try {
|
|
837
|
-
createBackup(resolvedPath);
|
|
838
|
-
}
|
|
839
|
-
catch (error) {
|
|
840
|
-
return errorResponse(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`);
|
|
841
|
-
}
|
|
842
|
-
try {
|
|
843
|
-
fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
restoreBackup(resolvedPath);
|
|
847
|
-
return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
|
|
848
|
-
}
|
|
849
|
-
return {
|
|
850
|
-
content: [
|
|
851
|
-
{
|
|
852
|
-
type: "text",
|
|
853
|
-
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}`,
|
|
854
|
-
},
|
|
855
|
-
],
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
// PHASE 1: Dry-Run
|
|
859
|
-
if (!code)
|
|
860
|
-
return errorResponse("Missing required parameter: code");
|
|
861
|
-
const resolvedPath = path.resolve(filePath);
|
|
862
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
863
|
-
return errorResponse(`File not found: ${resolvedPath}`);
|
|
864
|
-
}
|
|
865
|
-
const ext = path.extname(resolvedPath).toLowerCase();
|
|
866
|
-
if (!WRITABLE_EXTENSIONS.has(ext)) {
|
|
867
|
-
return errorResponse(`Unsupported file type "${ext}" for code insertion.`);
|
|
868
|
-
}
|
|
869
|
-
let fileContent;
|
|
870
|
-
try {
|
|
871
|
-
fileContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
872
|
-
}
|
|
873
|
-
catch (error) {
|
|
874
|
-
return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
|
|
875
|
-
}
|
|
876
|
-
const result = insertCode(resolvedPath, fileContent, code, anchorSymbol, position, className);
|
|
877
|
-
if (!result.success) {
|
|
878
|
-
return errorResponse(result.error || "Unknown error during code insertion");
|
|
879
|
-
}
|
|
880
|
-
const diff = generateDiff(fileContent, result.newContent, resolvedPath);
|
|
881
|
-
const token = confirmationCache.add("insert_symbol", resolvedPath, {
|
|
882
|
-
newContentFull: result.newContent,
|
|
883
|
-
diff,
|
|
884
|
-
anchorSymbol,
|
|
885
|
-
position
|
|
886
|
-
});
|
|
887
|
-
return {
|
|
888
|
-
content: [
|
|
889
|
-
{
|
|
890
|
-
type: "text",
|
|
891
|
-
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}`,
|
|
892
|
-
},
|
|
893
|
-
],
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
// ─── Tool 6: rename_symbol ──────────────────────────────────────────
|
|
897
|
-
export async function handleRenameSymbol(args) {
|
|
898
|
-
const filePath = args.filePath;
|
|
899
|
-
const oldName = args.oldName;
|
|
900
|
-
const newName = args.newName;
|
|
901
|
-
const rootDir = args.rootDir;
|
|
902
|
-
const shouldBackup = args.createBackup || false;
|
|
903
|
-
const confirmationToken = args.confirmationToken;
|
|
904
|
-
const confirm = args.confirm;
|
|
905
|
-
if (!filePath)
|
|
906
|
-
return errorResponse("Missing required parameter: filePath");
|
|
907
|
-
// PHASE 2
|
|
908
|
-
if (confirmationToken && confirm) {
|
|
909
|
-
const pending = confirmationCache.consume(confirmationToken);
|
|
910
|
-
if (!pending || pending.operation !== "rename_symbol") {
|
|
911
|
-
return errorResponse("Invalid or expired confirmation token.");
|
|
912
|
-
}
|
|
913
|
-
const results = pending.payload.results;
|
|
914
|
-
for (const res of results) {
|
|
915
|
-
try {
|
|
916
|
-
createBackup(res.filePath);
|
|
917
|
-
}
|
|
918
|
-
catch (e) { }
|
|
919
|
-
}
|
|
920
|
-
try {
|
|
921
|
-
for (const res of results) {
|
|
922
|
-
fs.writeFileSync(res.filePath, res.newContent, "utf-8");
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
catch (error) {
|
|
926
|
-
return errorResponse(`Failed to write files during rename: ${error instanceof Error ? error.message : String(error)}`);
|
|
927
|
-
}
|
|
928
|
-
return {
|
|
929
|
-
content: [{
|
|
930
|
-
type: "text",
|
|
931
|
-
text: `// ✅ Rename applied successfully.\n\n${pending.payload.diff}`,
|
|
932
|
-
}],
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
// PHASE 1
|
|
936
|
-
if (!oldName)
|
|
937
|
-
return errorResponse("Missing required parameter: oldName");
|
|
938
|
-
if (!newName)
|
|
939
|
-
return errorResponse("Missing required parameter: newName");
|
|
940
|
-
const resolvedPath = path.resolve(filePath);
|
|
941
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
942
|
-
return errorResponse(`File not found: ${resolvedPath}`);
|
|
943
|
-
}
|
|
944
|
-
let sourceContent;
|
|
945
|
-
try {
|
|
946
|
-
sourceContent = 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
|
-
// 1. Rename in the source file
|
|
952
|
-
const sourceResult = renameInFile(resolvedPath, sourceContent, oldName, newName);
|
|
953
|
-
if (!sourceResult.success) {
|
|
954
|
-
return errorResponse(sourceResult.error || `Could not find definition for "${oldName}" in ${path.basename(resolvedPath)}.`);
|
|
955
|
-
}
|
|
956
|
-
const results = [
|
|
957
|
-
{
|
|
958
|
-
filePath: resolvedPath,
|
|
959
|
-
oldContent: sourceContent,
|
|
960
|
-
newContent: sourceResult.newContent,
|
|
961
|
-
},
|
|
962
|
-
];
|
|
963
|
-
// 2. Discover and rename in dependent files
|
|
964
|
-
const projectRoot = rootDir ? path.resolve(rootDir) : findProjectRoot(resolvedPath);
|
|
965
|
-
if (projectRoot) {
|
|
966
|
-
const ignoreManager = new IgnoreManager(projectRoot);
|
|
967
|
-
const allFiles = ignoreManager.walkDirectory();
|
|
968
|
-
for (const dependentFile of allFiles) {
|
|
969
|
-
if (path.resolve(dependentFile) === resolvedPath)
|
|
970
|
-
continue;
|
|
971
|
-
const ext = path.extname(dependentFile).toLowerCase();
|
|
972
|
-
if (!IMPORTABLE_EXTENSIONS.has(ext))
|
|
973
|
-
continue;
|
|
974
|
-
let depContent;
|
|
975
|
-
try {
|
|
976
|
-
depContent = fs.readFileSync(dependentFile, "utf-8");
|
|
977
|
-
}
|
|
978
|
-
catch {
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
// Quick check if file contains oldName before attempting heavy rename
|
|
982
|
-
const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
983
|
-
const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`);
|
|
984
|
-
if (!regex.test(depContent))
|
|
985
|
-
continue;
|
|
986
|
-
const depResult = renameReferencesInFile(dependentFile, depContent, oldName, newName);
|
|
987
|
-
if (depResult.success && depResult.newContent !== depContent) {
|
|
988
|
-
results.push({
|
|
989
|
-
filePath: path.resolve(dependentFile),
|
|
990
|
-
oldContent: depContent,
|
|
991
|
-
newContent: depResult.newContent,
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
const multiDiff = generateMultiFileDiff(results);
|
|
997
|
-
const token = confirmationCache.add("rename_symbol", resolvedPath, {
|
|
998
|
-
results,
|
|
999
|
-
diff: multiDiff
|
|
1000
|
-
});
|
|
1001
|
-
return {
|
|
1002
|
-
content: [{
|
|
1003
|
-
type: "text",
|
|
1004
|
-
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}`,
|
|
1005
|
-
}],
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
// ─── Tool 7: remove_symbol ──────────────────────────────────────────
|
|
1009
|
-
export async function handleRemoveSymbol(args) {
|
|
1010
|
-
const filePath = args.filePath;
|
|
1011
|
-
const symbolName = args.symbolName;
|
|
1012
|
-
const className = args.className;
|
|
1013
|
-
const force = args.force || false;
|
|
1014
|
-
const shouldBackup = args.createBackup || false;
|
|
1015
|
-
const confirmationToken = args.confirmationToken;
|
|
1016
|
-
const confirm = args.confirm;
|
|
1017
|
-
if (!filePath)
|
|
1018
|
-
return errorResponse("Missing required parameter: filePath");
|
|
1019
|
-
// PHASE 2
|
|
1020
|
-
if (confirmationToken && confirm) {
|
|
1021
|
-
const pending = confirmationCache.consume(confirmationToken);
|
|
1022
|
-
if (!pending || pending.operation !== "remove_symbol") {
|
|
1023
|
-
return errorResponse("Invalid or expired confirmation token.");
|
|
1024
|
-
}
|
|
1025
|
-
const resolvedPath = pending.filePath;
|
|
1026
|
-
try {
|
|
1027
|
-
createBackup(resolvedPath);
|
|
1028
|
-
}
|
|
1029
|
-
catch (e) { }
|
|
1030
|
-
try {
|
|
1031
|
-
fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
|
|
1032
|
-
}
|
|
1033
|
-
catch (error) {
|
|
1034
|
-
restoreBackup(resolvedPath);
|
|
1035
|
-
return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1036
|
-
}
|
|
1037
|
-
return {
|
|
1038
|
-
content: [{
|
|
1039
|
-
type: "text",
|
|
1040
|
-
text: `// ✅ Symbol "${pending.payload.symbolName}" removed successfully.\n\n${pending.payload.diff}`,
|
|
1041
|
-
}],
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
if (!symbolName)
|
|
1045
|
-
return errorResponse("Missing required parameter: symbolName");
|
|
1046
|
-
const resolvedPath = path.resolve(filePath);
|
|
1047
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
1048
|
-
return errorResponse(`File not found: ${resolvedPath}`);
|
|
1049
|
-
}
|
|
1050
|
-
let fileContent;
|
|
1051
|
-
try {
|
|
1052
|
-
fileContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
1053
|
-
}
|
|
1054
|
-
catch (error) {
|
|
1055
|
-
return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1056
|
-
}
|
|
1057
|
-
// Safety check: Don't remove if dependents exist unless forced
|
|
1058
|
-
if (!force) {
|
|
1059
|
-
const projectRoot = findProjectRoot(resolvedPath);
|
|
1060
|
-
if (projectRoot) {
|
|
1061
|
-
const ignoreManager = new IgnoreManager(projectRoot);
|
|
1062
|
-
const allFiles = ignoreManager.walkDirectory();
|
|
1063
|
-
const targetRel = path.relative(projectRoot, resolvedPath).replace(/\\/g, "/");
|
|
1064
|
-
for (const f of allFiles) {
|
|
1065
|
-
if (path.resolve(f) === resolvedPath)
|
|
1066
|
-
continue;
|
|
1067
|
-
const fExt = path.extname(f).toLowerCase();
|
|
1068
|
-
if (!IMPORTABLE_EXTENSIONS.has(fExt))
|
|
1069
|
-
continue;
|
|
1070
|
-
try {
|
|
1071
|
-
const fContent = fs.readFileSync(f, "utf-8");
|
|
1072
|
-
const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1073
|
-
const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`);
|
|
1074
|
-
if (regex.test(fContent)) {
|
|
1075
|
-
return errorResponse(`Symbol "${symbolName}" might be used in ${path.relative(projectRoot, f)}. Use force: true to delete anyway.`);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
catch { }
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
const result = removeSymbolFromFile(resolvedPath, fileContent, symbolName, className);
|
|
1083
|
-
if (!result.success) {
|
|
1084
|
-
return errorResponse(result.error || "Unknown error during symbol removal");
|
|
1085
|
-
}
|
|
1086
|
-
const diff = generateDiff(fileContent, result.newContent, resolvedPath);
|
|
1087
|
-
const token = confirmationCache.add("remove_symbol", resolvedPath, {
|
|
1088
|
-
symbolName,
|
|
1089
|
-
newContentFull: result.newContent,
|
|
1090
|
-
diff
|
|
1091
|
-
});
|
|
1092
|
-
return {
|
|
1093
|
-
content: [{
|
|
1094
|
-
type: "text",
|
|
1095
|
-
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}`,
|
|
1096
|
-
}],
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
// ─── Import Resolution Logic ────────────────────────────────────────
|
|
1100
|
-
function resolveImportMatch(importPath, targetFile, sourceFile, rootDir) {
|
|
1101
|
-
const normalized = importPath.replace(/\\/g, "/");
|
|
1102
|
-
const targetResolved = path.resolve(targetFile);
|
|
1103
|
-
const targetWithoutExt = targetResolved.replace(/\.[^.]+$/, "");
|
|
1104
|
-
const targetBaseName = path.basename(targetFile, path.extname(targetFile));
|
|
1105
|
-
// Relative imports (./foo, ../bar)
|
|
1106
|
-
if (normalized.startsWith(".")) {
|
|
1107
|
-
const sourceDir = path.dirname(sourceFile);
|
|
1108
|
-
const resolved = path.resolve(sourceDir, normalized);
|
|
1109
|
-
// Try exact match, then with common extensions, then /index variants
|
|
1110
|
-
const candidates = [
|
|
1111
|
-
resolved,
|
|
1112
|
-
resolved + ".ts",
|
|
1113
|
-
resolved + ".tsx",
|
|
1114
|
-
resolved + ".js",
|
|
1115
|
-
resolved + ".jsx",
|
|
1116
|
-
resolved + ".mts",
|
|
1117
|
-
resolved + ".mjs",
|
|
1118
|
-
resolved + ".dart",
|
|
1119
|
-
path.join(resolved, "index.ts"),
|
|
1120
|
-
path.join(resolved, "index.tsx"),
|
|
1121
|
-
path.join(resolved, "index.js"),
|
|
1122
|
-
path.join(resolved, "index.mjs"),
|
|
1123
|
-
];
|
|
1124
|
-
for (const candidate of candidates) {
|
|
1125
|
-
if (path.resolve(candidate) === targetResolved ||
|
|
1126
|
-
path.resolve(candidate) === targetWithoutExt) {
|
|
1127
|
-
return true;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
return false;
|
|
1131
|
-
}
|
|
1132
|
-
// Non-relative imports (aliases, packages)
|
|
1133
|
-
// Check if the import path ends with the target's relative path or basename
|
|
1134
|
-
const targetRelative = path
|
|
1135
|
-
.relative(rootDir, targetFile)
|
|
1136
|
-
.replace(/\\/g, "/")
|
|
1137
|
-
.replace(/\.[^.]+$/, ""); // strip extension
|
|
1138
|
-
if (normalized === targetBaseName ||
|
|
1139
|
-
normalized.endsWith("/" + targetBaseName) ||
|
|
1140
|
-
normalized === targetRelative ||
|
|
1141
|
-
normalized.endsWith(targetRelative)) {
|
|
1142
|
-
return true;
|
|
1143
|
-
}
|
|
1144
|
-
// Python-style dotted imports: convert dots to path separators
|
|
1145
|
-
const dottedPath = normalized.replace(/\./g, "/");
|
|
1146
|
-
if (dottedPath === targetBaseName ||
|
|
1147
|
-
dottedPath.endsWith("/" + targetBaseName) ||
|
|
1148
|
-
dottedPath === targetRelative ||
|
|
1149
|
-
dottedPath.endsWith(targetRelative)) {
|
|
1150
|
-
return true;
|
|
1151
|
-
}
|
|
1152
|
-
return false;
|
|
1153
|
-
}
|
|
1154
|
-
// ─── Project Root Discovery ─────────────────────────────────────────
|
|
1155
|
-
function findProjectRoot(filePath) {
|
|
1156
|
-
const ROOT_MARKERS = [
|
|
1157
|
-
"package.json",
|
|
1158
|
-
".git",
|
|
1159
|
-
"pyproject.toml",
|
|
1160
|
-
"setup.py",
|
|
1161
|
-
"Cargo.toml",
|
|
1162
|
-
"go.mod",
|
|
1163
|
-
"pom.xml",
|
|
1164
|
-
"build.gradle",
|
|
1165
|
-
"CMakeLists.txt",
|
|
1166
|
-
".gitignore",
|
|
1167
|
-
"Gemfile",
|
|
1168
|
-
"composer.json",
|
|
1169
|
-
"pubspec.yaml",
|
|
1170
|
-
];
|
|
1171
|
-
let current = path.dirname(path.resolve(filePath));
|
|
1172
|
-
const root = path.parse(current).root;
|
|
1173
|
-
while (current !== root) {
|
|
1174
|
-
for (const marker of ROOT_MARKERS) {
|
|
1175
|
-
if (fs.existsSync(path.join(current, marker))) {
|
|
1176
|
-
return current;
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
current = path.dirname(current);
|
|
1180
|
-
}
|
|
1181
|
-
return undefined;
|
|
1182
|
-
}
|
|
1183
|
-
function formatAsXml(projectName, projectPath, entries, skippedCount) {
|
|
1184
|
-
const lines = [
|
|
1185
|
-
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
1186
|
-
`<repository name="${esc(projectName)}" path="${esc(projectPath)}" files="${entries.length}" skipped="${skippedCount}">`,
|
|
1187
|
-
``,
|
|
1188
|
-
` <file_tree>`,
|
|
1189
|
-
];
|
|
1190
|
-
for (const entry of entries) {
|
|
1191
|
-
lines.push(` <file path="${esc(entry.relativePath)}" language="${esc(entry.language)}" />`);
|
|
1192
|
-
}
|
|
1193
|
-
lines.push(` </file_tree>`);
|
|
1194
|
-
lines.push(``);
|
|
1195
|
-
for (const entry of entries) {
|
|
1196
|
-
lines.push(` <file path="${esc(entry.relativePath)}" language="${esc(entry.language)}">`);
|
|
1197
|
-
lines.push(`<compressed>`);
|
|
1198
|
-
lines.push(entry.compressed);
|
|
1199
|
-
lines.push(`</compressed>`);
|
|
1200
|
-
lines.push(` </file>`);
|
|
1201
|
-
lines.push(``);
|
|
1202
|
-
}
|
|
1203
|
-
lines.push(`</repository>`);
|
|
1204
|
-
return lines.join("\n");
|
|
1205
|
-
}
|
|
1206
|
-
function formatAsMarkdown(projectName, projectPath, entries, skippedCount) {
|
|
1207
|
-
const lines = [
|
|
1208
|
-
`# Repository Map: ${projectName}`,
|
|
1209
|
-
``,
|
|
1210
|
-
`> **Path:** \`${projectPath}\` `,
|
|
1211
|
-
`> **Source files:** ${entries.length} | **Skipped:** ${skippedCount}`,
|
|
1212
|
-
``,
|
|
1213
|
-
`## File Tree`,
|
|
1214
|
-
``,
|
|
1215
|
-
];
|
|
1216
|
-
for (const entry of entries) {
|
|
1217
|
-
lines.push(`- \`${entry.relativePath}\` *(${entry.language})*`);
|
|
1218
|
-
}
|
|
1219
|
-
lines.push(``);
|
|
1220
|
-
lines.push(`---`);
|
|
1221
|
-
lines.push(``);
|
|
1222
|
-
for (const entry of entries) {
|
|
1223
|
-
const langId = getLangId(entry.language);
|
|
1224
|
-
lines.push(`### ${entry.relativePath}`);
|
|
1225
|
-
lines.push("```" + langId);
|
|
1226
|
-
lines.push(entry.compressed);
|
|
1227
|
-
lines.push("```");
|
|
1228
|
-
lines.push(``);
|
|
1229
|
-
}
|
|
1230
|
-
return lines.join("\n");
|
|
1231
|
-
}
|
|
1232
|
-
function buildImpactReport(targetRelative, rootDir, dependents) {
|
|
1233
|
-
const lines = [
|
|
1234
|
-
`# Impact Analysis`,
|
|
1235
|
-
``,
|
|
1236
|
-
`## Target`,
|
|
1237
|
-
`- **File:** \`${targetRelative}\``,
|
|
1238
|
-
`- **Project root:** \`${rootDir}\``,
|
|
1239
|
-
`- **Dependent files:** ${dependents.length}`,
|
|
1240
|
-
``,
|
|
1241
|
-
];
|
|
1242
|
-
if (dependents.length === 0) {
|
|
1243
|
-
lines.push(`> No files in this repository import or depend on the target file.`);
|
|
1244
|
-
lines.push(``);
|
|
1245
|
-
lines.push(`This could mean the file is a leaf module, an entry point, ` +
|
|
1246
|
-
`or uses non-standard import patterns not detected by this analysis.`);
|
|
1247
|
-
}
|
|
1248
|
-
else {
|
|
1249
|
-
lines.push(`## Dependent Files`);
|
|
1250
|
-
lines.push(``);
|
|
1251
|
-
for (const dep of dependents) {
|
|
1252
|
-
lines.push(`### \`${dep.file}\``);
|
|
1253
|
-
for (const imp of dep.imports) {
|
|
1254
|
-
lines.push(`- \`${imp}\``);
|
|
1255
|
-
}
|
|
1256
|
-
lines.push(``);
|
|
1257
|
-
}
|
|
1258
|
-
lines.push(`---`);
|
|
1259
|
-
lines.push(``);
|
|
1260
|
-
lines.push(`> **⚠️ Blast radius:** Modifying \`${targetRelative}\` may affect ` +
|
|
1261
|
-
`**${dependents.length}** file(s). Review the import statements above ` +
|
|
1262
|
-
`to understand what each dependent file consumes from the target.`);
|
|
1263
|
-
}
|
|
1264
|
-
return lines.join("\n");
|
|
1265
|
-
}
|
|
1266
451
|
// ─── Utility ────────────────────────────────────────────────────────
|
|
1267
|
-
function getLanguageName(ext) {
|
|
1268
|
-
const map = {
|
|
1269
|
-
".ts": "TypeScript",
|
|
1270
|
-
".tsx": "TypeScript JSX",
|
|
1271
|
-
".mts": "TypeScript",
|
|
1272
|
-
".cts": "TypeScript",
|
|
1273
|
-
".js": "JavaScript",
|
|
1274
|
-
".jsx": "JavaScript JSX",
|
|
1275
|
-
".mjs": "JavaScript",
|
|
1276
|
-
".cjs": "JavaScript",
|
|
1277
|
-
".py": "Python",
|
|
1278
|
-
".pyi": "Python Stub",
|
|
1279
|
-
".json": "JSON",
|
|
1280
|
-
".yaml": "YAML",
|
|
1281
|
-
".yml": "YAML",
|
|
1282
|
-
".toml": "TOML",
|
|
1283
|
-
".md": "Markdown",
|
|
1284
|
-
".txt": "Text",
|
|
1285
|
-
".css": "CSS",
|
|
1286
|
-
".scss": "SCSS",
|
|
1287
|
-
".less": "LESS",
|
|
1288
|
-
".html": "HTML",
|
|
1289
|
-
".vue": "Vue",
|
|
1290
|
-
".svelte": "Svelte",
|
|
1291
|
-
".rs": "Rust",
|
|
1292
|
-
".go": "Go",
|
|
1293
|
-
".java": "Java",
|
|
1294
|
-
".kt": "Kotlin",
|
|
1295
|
-
".c": "C",
|
|
1296
|
-
".cpp": "C++",
|
|
1297
|
-
".h": "C Header",
|
|
1298
|
-
".hpp": "C++ Header",
|
|
1299
|
-
".rb": "Ruby",
|
|
1300
|
-
".php": "PHP",
|
|
1301
|
-
".dart": "Dart",
|
|
1302
|
-
".swift": "Swift",
|
|
1303
|
-
".sh": "Shell",
|
|
1304
|
-
".bash": "Bash",
|
|
1305
|
-
".zsh": "Zsh",
|
|
1306
|
-
".sql": "SQL",
|
|
1307
|
-
".graphql": "GraphQL",
|
|
1308
|
-
".gql": "GraphQL",
|
|
1309
|
-
".proto": "Protocol Buffers",
|
|
1310
|
-
".dockerfile": "Dockerfile",
|
|
1311
|
-
};
|
|
1312
|
-
return map[ext] || ext.substring(1).toUpperCase();
|
|
1313
|
-
}
|
|
1314
|
-
function getLangId(language) {
|
|
1315
|
-
const map = {
|
|
1316
|
-
TypeScript: "typescript",
|
|
1317
|
-
"TypeScript JSX": "tsx",
|
|
1318
|
-
JavaScript: "javascript",
|
|
1319
|
-
"JavaScript JSX": "jsx",
|
|
1320
|
-
Python: "python",
|
|
1321
|
-
"Python Stub": "python",
|
|
1322
|
-
JSON: "json",
|
|
1323
|
-
YAML: "yaml",
|
|
1324
|
-
TOML: "toml",
|
|
1325
|
-
Markdown: "markdown",
|
|
1326
|
-
CSS: "css",
|
|
1327
|
-
SCSS: "scss",
|
|
1328
|
-
LESS: "less",
|
|
1329
|
-
HTML: "html",
|
|
1330
|
-
Vue: "vue",
|
|
1331
|
-
Svelte: "svelte",
|
|
1332
|
-
Rust: "rust",
|
|
1333
|
-
Go: "go",
|
|
1334
|
-
Java: "java",
|
|
1335
|
-
Kotlin: "kotlin",
|
|
1336
|
-
C: "c",
|
|
1337
|
-
"C++": "cpp",
|
|
1338
|
-
Ruby: "ruby",
|
|
1339
|
-
PHP: "php",
|
|
1340
|
-
Dart: "dart",
|
|
1341
|
-
Swift: "swift",
|
|
1342
|
-
Shell: "bash",
|
|
1343
|
-
SQL: "sql",
|
|
1344
|
-
GraphQL: "graphql",
|
|
1345
|
-
};
|
|
1346
|
-
return map[language] || language.toLowerCase();
|
|
1347
|
-
}
|
|
1348
|
-
function esc(str) {
|
|
1349
|
-
return str
|
|
1350
|
-
.replace(/&/g, "&")
|
|
1351
|
-
.replace(/</g, "<")
|
|
1352
|
-
.replace(/>/g, ">")
|
|
1353
|
-
.replace(/"/g, """)
|
|
1354
|
-
.replace(/'/g, "'");
|
|
1355
|
-
}
|
|
1356
|
-
export async function handleRollbackFile(args) {
|
|
1357
|
-
const filePath = args.filePath;
|
|
1358
|
-
const steps = args.steps || 1;
|
|
1359
|
-
if (!filePath)
|
|
1360
|
-
return errorResponse("Missing required parameter: filePath");
|
|
1361
|
-
const resolvedPath = path.resolve(filePath);
|
|
1362
|
-
if (steps < 1 || steps > 5) {
|
|
1363
|
-
return errorResponse("Steps must be between 1 and 5.");
|
|
1364
|
-
}
|
|
1365
|
-
const success = restoreBackup(resolvedPath, steps);
|
|
1366
|
-
if (!success) {
|
|
1367
|
-
return errorResponse(`No backup found for step ${steps} at ${resolvedPath}`);
|
|
1368
|
-
}
|
|
1369
|
-
return {
|
|
1370
|
-
content: [{
|
|
1371
|
-
type: "text",
|
|
1372
|
-
text: `// ⏪ Rollback successful!\n// Reverted ${path.basename(resolvedPath)} to state from ${steps} step(s) ago.\n// File: ${resolvedPath}`,
|
|
1373
|
-
}],
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
export async function handleCleanBackups(args) {
|
|
1377
|
-
const projectRoot = args.projectRoot;
|
|
1378
|
-
if (!projectRoot)
|
|
1379
|
-
return errorResponse("Missing required parameter: projectRoot");
|
|
1380
|
-
const resolvedPath = path.resolve(projectRoot);
|
|
1381
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
1382
|
-
return errorResponse(`Project root not found: ${resolvedPath}`);
|
|
1383
|
-
}
|
|
1384
|
-
const success = cleanAllBackups(resolvedPath);
|
|
1385
|
-
if (!success) {
|
|
1386
|
-
return {
|
|
1387
|
-
content: [{
|
|
1388
|
-
type: "text",
|
|
1389
|
-
text: `// ℹ️ No backups found\n// No .mcp-backups directory exists at: ${resolvedPath}`,
|
|
1390
|
-
}],
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
return {
|
|
1394
|
-
content: [{
|
|
1395
|
-
type: "text",
|
|
1396
|
-
text: `// 🧹 Cleanup successful!\n// Removed all backups from: ${resolvedPath}/.mcp-backups\n// Your project is now clean.`,
|
|
1397
|
-
}],
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
// ─── Tool: read_file_lines ──────────────────────────────────────────
|
|
1401
|
-
export async function handleReadFileLines(args) {
|
|
1402
|
-
const result = readFileLines(args);
|
|
1403
|
-
if (!result.success) {
|
|
1404
|
-
return errorResponse(result.error || "Unknown error reading file lines");
|
|
1405
|
-
}
|
|
1406
|
-
const { content, lineRange } = result;
|
|
1407
|
-
const header = lineRange
|
|
1408
|
-
? `// Lines ${lineRange.start}-${lineRange.end}\n// File: ${args.filePath}\n\n`
|
|
1409
|
-
: `// File: ${args.filePath}\n\n`;
|
|
1410
|
-
return {
|
|
1411
|
-
content: [
|
|
1412
|
-
{
|
|
1413
|
-
type: "text",
|
|
1414
|
-
text: header + content,
|
|
1415
|
-
},
|
|
1416
|
-
],
|
|
1417
|
-
};
|
|
1418
|
-
}
|
|
1419
|
-
// ─── Tool: search_code_pattern ──────────────────────────────────────
|
|
1420
|
-
export async function handleSearchCodePattern(args) {
|
|
1421
|
-
const result = searchCodePattern(args);
|
|
1422
|
-
if (!result.success) {
|
|
1423
|
-
return errorResponse(result.error || "Unknown error searching code");
|
|
1424
|
-
}
|
|
1425
|
-
const { matches, totalMatches } = result;
|
|
1426
|
-
if (!matches || matches.length === 0) {
|
|
1427
|
-
return {
|
|
1428
|
-
content: [
|
|
1429
|
-
{
|
|
1430
|
-
type: "text",
|
|
1431
|
-
text: `# Search Results\n\nNo matches found for pattern: "${args.pattern}"`,
|
|
1432
|
-
},
|
|
1433
|
-
],
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
const lines = [
|
|
1437
|
-
`# Search Results`,
|
|
1438
|
-
``,
|
|
1439
|
-
`**Pattern:** \`${args.pattern}\``,
|
|
1440
|
-
`**Root:** \`${args.rootDir}\``,
|
|
1441
|
-
`**Matches:** ${totalMatches} (showing ${matches.length})`,
|
|
1442
|
-
``,
|
|
1443
|
-
];
|
|
1444
|
-
for (const match of matches) {
|
|
1445
|
-
lines.push(`## \`${match.file}\` (line ${match.lineNumber})`);
|
|
1446
|
-
lines.push(``);
|
|
1447
|
-
if (match.context) {
|
|
1448
|
-
lines.push("```");
|
|
1449
|
-
lines.push(...match.context);
|
|
1450
|
-
lines.push("```");
|
|
1451
|
-
}
|
|
1452
|
-
else {
|
|
1453
|
-
lines.push(`\`\`\`\n${match.line}\n\`\`\``);
|
|
1454
|
-
}
|
|
1455
|
-
lines.push(``);
|
|
1456
|
-
}
|
|
1457
|
-
return {
|
|
1458
|
-
content: [
|
|
1459
|
-
{
|
|
1460
|
-
type: "text",
|
|
1461
|
-
text: lines.join("\n"),
|
|
1462
|
-
},
|
|
1463
|
-
],
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
452
|
function errorResponse(message) {
|
|
1467
453
|
return {
|
|
1468
454
|
content: [{ type: "text", text: `❌ ${message}` }],
|