@reverse-craft/smart-fs 1.0.2 → 1.0.4
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/README.md +2 -2
- package/dist/server.js +820 -102
- package/dist/server.js.map +4 -4
- package/package.json +9 -1
package/dist/server.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/server.ts
|
|
4
|
-
import {
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { z as z5 } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/tools/readCodeSmart.ts
|
|
10
9
|
import { z } from "zod";
|
|
11
10
|
import { SourceMapConsumer } from "source-map-js";
|
|
12
11
|
|
|
12
|
+
// src/tools/ToolDefinition.ts
|
|
13
|
+
function defineTool(definition) {
|
|
14
|
+
return definition;
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
// src/beautifier.ts
|
|
14
18
|
import * as esbuild from "esbuild";
|
|
15
19
|
import * as crypto from "crypto";
|
|
16
20
|
import * as fs from "fs/promises";
|
|
17
21
|
import * as path from "path";
|
|
18
22
|
import * as os from "os";
|
|
19
|
-
var TEMP_DIR = path.join(os.tmpdir(), "
|
|
23
|
+
var TEMP_DIR = path.join(os.tmpdir(), "smart-fs-mcp-cache");
|
|
20
24
|
async function ensureCacheDir() {
|
|
21
25
|
await fs.mkdir(TEMP_DIR, { recursive: true });
|
|
22
26
|
}
|
|
@@ -30,6 +34,15 @@ function getCachePaths(originalPath, hash) {
|
|
|
30
34
|
const mapPath = `${beautifiedPath}.map`;
|
|
31
35
|
return { beautifiedPath, mapPath };
|
|
32
36
|
}
|
|
37
|
+
function getLocalPaths(originalPath) {
|
|
38
|
+
const absolutePath = path.resolve(originalPath);
|
|
39
|
+
const dir = path.dirname(absolutePath);
|
|
40
|
+
const ext = path.extname(absolutePath);
|
|
41
|
+
const baseName = path.basename(absolutePath, ext);
|
|
42
|
+
const beautifiedPath = path.join(dir, `${baseName}.beautified.js`);
|
|
43
|
+
const mapPath = `${beautifiedPath}.map`;
|
|
44
|
+
return { beautifiedPath, mapPath };
|
|
45
|
+
}
|
|
33
46
|
async function isCacheValid(beautifiedPath, mapPath) {
|
|
34
47
|
try {
|
|
35
48
|
await Promise.all([
|
|
@@ -41,14 +54,69 @@ async function isCacheValid(beautifiedPath, mapPath) {
|
|
|
41
54
|
return false;
|
|
42
55
|
}
|
|
43
56
|
}
|
|
44
|
-
async function
|
|
57
|
+
async function isLocalCacheValid(originalPath) {
|
|
45
58
|
const absolutePath = path.resolve(originalPath);
|
|
59
|
+
const { beautifiedPath } = getLocalPaths(absolutePath);
|
|
60
|
+
let originalStats;
|
|
61
|
+
try {
|
|
62
|
+
originalStats = await fs.stat(absolutePath);
|
|
63
|
+
} catch {
|
|
64
|
+
return {
|
|
65
|
+
originalMtime: 0,
|
|
66
|
+
beautifiedExists: false,
|
|
67
|
+
beautifiedMtime: 0,
|
|
68
|
+
isValid: false
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const originalMtime = originalStats.mtimeMs;
|
|
72
|
+
let beautifiedStats;
|
|
73
|
+
try {
|
|
74
|
+
beautifiedStats = await fs.stat(beautifiedPath);
|
|
75
|
+
} catch {
|
|
76
|
+
return {
|
|
77
|
+
originalMtime,
|
|
78
|
+
beautifiedExists: false,
|
|
79
|
+
beautifiedMtime: 0,
|
|
80
|
+
isValid: false
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const beautifiedMtime = beautifiedStats.mtimeMs;
|
|
84
|
+
const isValid = beautifiedMtime >= originalMtime;
|
|
85
|
+
return {
|
|
86
|
+
originalMtime,
|
|
87
|
+
beautifiedExists: true,
|
|
88
|
+
beautifiedMtime,
|
|
89
|
+
isValid
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function ensureBeautified(originalPath, options) {
|
|
93
|
+
const absolutePath = path.resolve(originalPath);
|
|
94
|
+
const saveLocal = options?.saveLocal ?? false;
|
|
46
95
|
let stats;
|
|
47
96
|
try {
|
|
48
97
|
stats = await fs.stat(absolutePath);
|
|
49
98
|
} catch {
|
|
50
99
|
throw new Error(`File not found: ${originalPath}`);
|
|
51
100
|
}
|
|
101
|
+
const localPaths = getLocalPaths(absolutePath);
|
|
102
|
+
if (saveLocal) {
|
|
103
|
+
const localCacheCheck = await isLocalCacheValid(absolutePath);
|
|
104
|
+
if (localCacheCheck.isValid) {
|
|
105
|
+
try {
|
|
106
|
+
const [code2, mapContent] = await Promise.all([
|
|
107
|
+
fs.readFile(localPaths.beautifiedPath, "utf-8"),
|
|
108
|
+
fs.readFile(localPaths.mapPath, "utf-8")
|
|
109
|
+
]);
|
|
110
|
+
return {
|
|
111
|
+
code: code2,
|
|
112
|
+
rawMap: JSON.parse(mapContent),
|
|
113
|
+
localPath: localPaths.beautifiedPath,
|
|
114
|
+
localMapPath: localPaths.mapPath
|
|
115
|
+
};
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
52
120
|
await ensureCacheDir();
|
|
53
121
|
const hash = calculateCacheKey(absolutePath, stats.mtimeMs);
|
|
54
122
|
const { beautifiedPath, mapPath } = getCachePaths(absolutePath, hash);
|
|
@@ -57,14 +125,18 @@ async function ensureBeautified(originalPath) {
|
|
|
57
125
|
fs.readFile(beautifiedPath, "utf-8"),
|
|
58
126
|
fs.readFile(mapPath, "utf-8")
|
|
59
127
|
]);
|
|
60
|
-
|
|
128
|
+
const result2 = {
|
|
61
129
|
code: code2,
|
|
62
130
|
rawMap: JSON.parse(mapContent)
|
|
63
131
|
};
|
|
132
|
+
if (saveLocal) {
|
|
133
|
+
await saveToLocal(result2, localPaths, mapContent);
|
|
134
|
+
}
|
|
135
|
+
return result2;
|
|
64
136
|
}
|
|
65
|
-
let
|
|
137
|
+
let esbuildResult;
|
|
66
138
|
try {
|
|
67
|
-
|
|
139
|
+
esbuildResult = await esbuild.build({
|
|
68
140
|
entryPoints: [absolutePath],
|
|
69
141
|
bundle: false,
|
|
70
142
|
write: false,
|
|
@@ -81,18 +153,42 @@ async function ensureBeautified(originalPath) {
|
|
|
81
153
|
const message = err instanceof Error ? err.message : String(err);
|
|
82
154
|
throw new Error(`Esbuild processing failed: ${message}`);
|
|
83
155
|
}
|
|
84
|
-
const codeFile =
|
|
85
|
-
const mapFile =
|
|
156
|
+
const codeFile = esbuildResult.outputFiles?.find((f) => f.path.endsWith(".js"));
|
|
157
|
+
const mapFile = esbuildResult.outputFiles?.find((f) => f.path.endsWith(".map"));
|
|
86
158
|
if (!codeFile || !mapFile) {
|
|
87
159
|
throw new Error("Esbuild processing failed: Missing output files");
|
|
88
160
|
}
|
|
89
161
|
const code = codeFile.text;
|
|
90
162
|
const rawMap = JSON.parse(mapFile.text);
|
|
163
|
+
const mapText = mapFile.text;
|
|
91
164
|
await Promise.all([
|
|
92
165
|
fs.writeFile(beautifiedPath, code, "utf-8"),
|
|
93
|
-
fs.writeFile(mapPath,
|
|
166
|
+
fs.writeFile(mapPath, mapText, "utf-8")
|
|
94
167
|
]);
|
|
95
|
-
|
|
168
|
+
const result = { code, rawMap };
|
|
169
|
+
if (saveLocal) {
|
|
170
|
+
await saveToLocal(result, localPaths, mapText);
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
async function saveToLocal(result, localPaths, mapText) {
|
|
175
|
+
try {
|
|
176
|
+
await Promise.all([
|
|
177
|
+
fs.writeFile(localPaths.beautifiedPath, result.code, "utf-8"),
|
|
178
|
+
fs.writeFile(localPaths.mapPath, mapText, "utf-8")
|
|
179
|
+
]);
|
|
180
|
+
result.localPath = localPaths.beautifiedPath;
|
|
181
|
+
result.localMapPath = localPaths.mapPath;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const error = err;
|
|
184
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
185
|
+
result.localSaveError = `Permission denied: Cannot write to ${path.dirname(localPaths.beautifiedPath)}`;
|
|
186
|
+
} else if (error.code === "ENOSPC") {
|
|
187
|
+
result.localSaveError = `Insufficient disk space: Cannot write to ${path.dirname(localPaths.beautifiedPath)}`;
|
|
188
|
+
} else {
|
|
189
|
+
result.localSaveError = `Failed to save locally: ${error.message || String(err)}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
96
192
|
}
|
|
97
193
|
|
|
98
194
|
// src/truncator.ts
|
|
@@ -157,25 +253,26 @@ function truncateCodeHighPerf(sourceCode, limit = 200, previewLength = 50) {
|
|
|
157
253
|
});
|
|
158
254
|
return magicString.toString();
|
|
159
255
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
file_path: z.string().describe("Path to the JavaScript file"),
|
|
164
|
-
start_line: z.number().int().min(1).describe("Start line number (1-based)"),
|
|
165
|
-
end_line: z.number().int().min(1).describe("End line number (1-based)"),
|
|
166
|
-
char_limit: z.number().int().min(50).default(300).describe("Character limit for string truncation")
|
|
167
|
-
});
|
|
168
|
-
var server = new Server(
|
|
169
|
-
{
|
|
170
|
-
name: "jsvmp-smart-fs",
|
|
171
|
-
version: "1.0.0"
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
capabilities: {
|
|
175
|
-
tools: {}
|
|
176
|
-
}
|
|
256
|
+
function truncateLongLines(code, maxLineChars = 500, previewRatio = 0.2) {
|
|
257
|
+
if (!code) {
|
|
258
|
+
return code;
|
|
177
259
|
}
|
|
178
|
-
);
|
|
260
|
+
const lines = code.split("\n");
|
|
261
|
+
const previewLength = Math.floor(maxLineChars * previewRatio);
|
|
262
|
+
const processedLines = lines.map((line) => {
|
|
263
|
+
if (line.length <= maxLineChars) {
|
|
264
|
+
return line;
|
|
265
|
+
}
|
|
266
|
+
const start = line.slice(0, previewLength);
|
|
267
|
+
const end = line.slice(-previewLength);
|
|
268
|
+
const truncatedChars = line.length - previewLength * 2;
|
|
269
|
+
const marker = `...[LINE TRUNCATED ${truncatedChars} CHARS]...`;
|
|
270
|
+
return `${start}${marker}${end}`;
|
|
271
|
+
});
|
|
272
|
+
return processedLines.join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/tools/readCodeSmart.ts
|
|
179
276
|
function formatSourcePosition(line, column) {
|
|
180
277
|
if (line !== null && column !== null) {
|
|
181
278
|
return `L${line}:${column}`;
|
|
@@ -200,95 +297,716 @@ function formatPaginationHint(nextStartLine) {
|
|
|
200
297
|
return `
|
|
201
298
|
... (Use next start_line=${nextStartLine} to read more)`;
|
|
202
299
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
300
|
+
var readCodeSmart = defineTool({
|
|
301
|
+
name: "read_code_smart",
|
|
302
|
+
description: "Read and beautify minified/obfuscated JavaScript code with source map coordinates. Returns formatted code with original file positions for setting breakpoints. Optionally saves the beautified file locally alongside the original file.",
|
|
303
|
+
schema: {
|
|
304
|
+
file_path: z.string().describe("Path to the JavaScript file"),
|
|
305
|
+
start_line: z.number().int().min(1).describe("Start line number (1-based)"),
|
|
306
|
+
end_line: z.number().int().min(1).describe("End line number (1-based)"),
|
|
307
|
+
char_limit: z.number().int().min(50).default(300).describe("Character limit for string truncation"),
|
|
308
|
+
max_line_chars: z.number().int().min(80).default(500).describe("Maximum characters per line"),
|
|
309
|
+
save_local: z.boolean().optional().default(false).describe("Save beautified file to the same directory as the original file")
|
|
310
|
+
},
|
|
311
|
+
handler: async (params) => {
|
|
312
|
+
const { file_path, start_line, end_line, char_limit, max_line_chars, save_local } = params;
|
|
313
|
+
const beautifyResult = await ensureBeautified(file_path, { saveLocal: save_local });
|
|
314
|
+
const { code, rawMap, localPath, localMapPath, localSaveError } = beautifyResult;
|
|
315
|
+
const truncatedCode = truncateCodeHighPerf(code, char_limit);
|
|
316
|
+
const lineTruncatedCode = truncateLongLines(truncatedCode, max_line_chars);
|
|
317
|
+
const lines = lineTruncatedCode.split("\n");
|
|
318
|
+
const totalLines = lines.length;
|
|
319
|
+
const effectiveStartLine = Math.max(1, start_line);
|
|
320
|
+
const effectiveEndLine = Math.min(totalLines, end_line);
|
|
321
|
+
if (effectiveStartLine > totalLines) {
|
|
322
|
+
throw new Error(`Start line ${start_line} exceeds total lines ${totalLines}`);
|
|
323
|
+
}
|
|
324
|
+
const consumer = new SourceMapConsumer({
|
|
325
|
+
...rawMap,
|
|
326
|
+
version: String(rawMap.version)
|
|
327
|
+
});
|
|
328
|
+
const outputParts = [];
|
|
329
|
+
outputParts.push(formatHeader(file_path, effectiveStartLine, effectiveEndLine, totalLines));
|
|
330
|
+
if (save_local) {
|
|
331
|
+
if (localPath) {
|
|
332
|
+
outputParts.push(`LOCAL: ${localPath}`);
|
|
333
|
+
if (localMapPath) {
|
|
334
|
+
outputParts.push(`LOCAL_MAP: ${localMapPath}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (localSaveError) {
|
|
338
|
+
outputParts.push(`LOCAL_SAVE_ERROR: ${localSaveError}`);
|
|
339
|
+
}
|
|
340
|
+
outputParts.push("-".repeat(85));
|
|
341
|
+
}
|
|
342
|
+
const maxLineNumWidth = String(effectiveEndLine).length;
|
|
343
|
+
for (let lineNum = effectiveStartLine; lineNum <= effectiveEndLine; lineNum++) {
|
|
344
|
+
const lineIndex = lineNum - 1;
|
|
345
|
+
const lineContent = lines[lineIndex] ?? "";
|
|
346
|
+
const originalPos = consumer.originalPositionFor({
|
|
347
|
+
line: lineNum,
|
|
348
|
+
column: 0
|
|
349
|
+
});
|
|
350
|
+
const sourcePos = formatSourcePosition(originalPos.line, originalPos.column);
|
|
351
|
+
outputParts.push(formatCodeLine(lineNum, sourcePos, lineContent, maxLineNumWidth));
|
|
352
|
+
}
|
|
353
|
+
if (effectiveEndLine < totalLines) {
|
|
354
|
+
outputParts.push(formatPaginationHint(effectiveEndLine + 1));
|
|
355
|
+
}
|
|
356
|
+
return outputParts.join("\n");
|
|
213
357
|
}
|
|
214
|
-
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// src/tools/applyCustomTransform.ts
|
|
361
|
+
import { z as z2 } from "zod";
|
|
362
|
+
|
|
363
|
+
// src/transformer.ts
|
|
364
|
+
import * as path2 from "path";
|
|
365
|
+
import * as fs2 from "fs/promises";
|
|
366
|
+
import { transformSync } from "@babel/core";
|
|
367
|
+
function cleanBasename(filename) {
|
|
368
|
+
const base = path2.basename(filename);
|
|
369
|
+
let name = base.endsWith(".js") ? base.slice(0, -3) : base;
|
|
370
|
+
name = name.replace(/_deob[^/]*$/, "");
|
|
371
|
+
if (name.endsWith(".beautified")) {
|
|
372
|
+
name = name.slice(0, -".beautified".length);
|
|
373
|
+
}
|
|
374
|
+
return name;
|
|
375
|
+
}
|
|
376
|
+
function getOutputPaths(targetFile, outputSuffix = "_deob") {
|
|
377
|
+
const absolutePath = path2.resolve(targetFile);
|
|
378
|
+
const dir = path2.dirname(absolutePath);
|
|
379
|
+
const basename3 = cleanBasename(absolutePath);
|
|
380
|
+
const outputPath = path2.join(dir, `${basename3}${outputSuffix}.js`);
|
|
381
|
+
const mapPath = `${outputPath}.map`;
|
|
382
|
+
return { outputPath, mapPath };
|
|
383
|
+
}
|
|
384
|
+
async function loadBabelPlugin(scriptPath) {
|
|
385
|
+
const absolutePath = path2.resolve(scriptPath);
|
|
386
|
+
try {
|
|
387
|
+
await fs2.access(absolutePath);
|
|
388
|
+
} catch {
|
|
389
|
+
throw new Error(`Script not found: ${absolutePath}`);
|
|
390
|
+
}
|
|
391
|
+
const fileUrl = `file://${absolutePath}?t=${Date.now()}`;
|
|
392
|
+
let module;
|
|
393
|
+
try {
|
|
394
|
+
module = await import(fileUrl);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
397
|
+
throw new Error(`Failed to load script: ${message}`);
|
|
398
|
+
}
|
|
399
|
+
const plugin = module.default ?? module;
|
|
400
|
+
if (typeof plugin !== "function") {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Invalid Babel plugin: Script must export a function that returns a visitor object. Got ${typeof plugin} instead.`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return plugin;
|
|
406
|
+
}
|
|
407
|
+
function runBabelTransform(code, inputSourceMap, plugin, filename) {
|
|
408
|
+
let result;
|
|
409
|
+
try {
|
|
410
|
+
result = transformSync(code, {
|
|
411
|
+
filename,
|
|
412
|
+
plugins: [plugin],
|
|
413
|
+
// Source map configuration for cascade
|
|
414
|
+
// @ts-expect-error - SourceMap is compatible with InputSourceMap at runtime
|
|
415
|
+
inputSourceMap,
|
|
416
|
+
sourceMaps: true,
|
|
417
|
+
// Readability settings
|
|
418
|
+
retainLines: false,
|
|
419
|
+
compact: false,
|
|
420
|
+
minified: false,
|
|
421
|
+
// Preserve code structure
|
|
422
|
+
parserOpts: {
|
|
423
|
+
sourceType: "unambiguous"
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
} catch (err) {
|
|
427
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
428
|
+
throw new Error(`Babel Error: ${message}`);
|
|
429
|
+
}
|
|
430
|
+
if (!result || !result.code) {
|
|
431
|
+
throw new Error("Babel Error: Transform produced no output");
|
|
432
|
+
}
|
|
433
|
+
if (!result.map) {
|
|
434
|
+
throw new Error("Babel Error: Transform produced no source map");
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
code: result.code,
|
|
438
|
+
map: result.map
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async function applyCustomTransform(targetFile, options) {
|
|
442
|
+
const { scriptPath, outputSuffix = "_deob" } = options;
|
|
443
|
+
const absoluteTargetPath = path2.resolve(targetFile);
|
|
444
|
+
try {
|
|
445
|
+
await fs2.access(absoluteTargetPath);
|
|
446
|
+
} catch {
|
|
447
|
+
throw new Error(`File not found: ${targetFile}`);
|
|
448
|
+
}
|
|
449
|
+
const plugin = await loadBabelPlugin(scriptPath);
|
|
450
|
+
const beautifyResult = await ensureBeautified(absoluteTargetPath);
|
|
451
|
+
const { code: beautifiedCode, rawMap: inputSourceMap } = beautifyResult;
|
|
452
|
+
const transformResult = runBabelTransform(
|
|
453
|
+
beautifiedCode,
|
|
454
|
+
inputSourceMap,
|
|
455
|
+
plugin,
|
|
456
|
+
absoluteTargetPath
|
|
457
|
+
);
|
|
458
|
+
const { outputPath, mapPath } = getOutputPaths(absoluteTargetPath, outputSuffix);
|
|
459
|
+
const mapFileName = path2.basename(mapPath);
|
|
460
|
+
const outputCode = `${transformResult.code}
|
|
461
|
+
//# sourceMappingURL=${mapFileName}`;
|
|
462
|
+
const outputMap = {
|
|
463
|
+
...transformResult.map,
|
|
464
|
+
file: path2.basename(outputPath)
|
|
465
|
+
};
|
|
466
|
+
try {
|
|
467
|
+
await Promise.all([
|
|
468
|
+
fs2.writeFile(outputPath, outputCode, "utf-8"),
|
|
469
|
+
fs2.writeFile(mapPath, JSON.stringify(outputMap, null, 2), "utf-8")
|
|
470
|
+
]);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
const error = err;
|
|
473
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
474
|
+
throw new Error(`Permission denied: Cannot write to ${path2.dirname(outputPath)}`);
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`Failed to write output files: ${error.message || String(err)}`);
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
code: outputCode,
|
|
480
|
+
map: outputMap,
|
|
481
|
+
outputPath,
|
|
482
|
+
mapPath
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/tools/applyCustomTransform.ts
|
|
487
|
+
var ApplyCustomTransformInputSchema = z2.object({
|
|
488
|
+
target_file: z2.string().describe("Path to the JavaScript file to transform"),
|
|
489
|
+
script_path: z2.string().describe("Path to a JS file exporting a Babel Plugin function"),
|
|
490
|
+
output_suffix: z2.string().default("_deob").describe("Suffix for output file name")
|
|
491
|
+
});
|
|
492
|
+
var applyCustomTransform2 = defineTool({
|
|
493
|
+
name: "apply_custom_transform",
|
|
494
|
+
description: "Apply a custom Babel transformation to deobfuscate JavaScript code. Takes a target JS file and a Babel plugin script, runs the transformation, and outputs the deobfuscated code with a cascaded source map that traces back to the original minified file. The plugin script should export a function that returns a Babel visitor object.",
|
|
495
|
+
schema: {
|
|
496
|
+
target_file: z2.string().describe("Path to the JavaScript file to transform"),
|
|
497
|
+
script_path: z2.string().describe("Path to a JS file exporting a Babel Plugin function"),
|
|
498
|
+
output_suffix: z2.string().default("_deob").describe("Suffix for output file name")
|
|
499
|
+
},
|
|
500
|
+
handler: async (params) => {
|
|
501
|
+
const { target_file, script_path, output_suffix } = params;
|
|
502
|
+
const result = await applyCustomTransform(target_file, {
|
|
503
|
+
scriptPath: script_path,
|
|
504
|
+
outputSuffix: output_suffix
|
|
505
|
+
});
|
|
506
|
+
const successMessage = [
|
|
507
|
+
"Transform completed successfully!",
|
|
508
|
+
"",
|
|
509
|
+
`Output file: ${result.outputPath}`,
|
|
510
|
+
`Source map: ${result.mapPath}`
|
|
511
|
+
].join("\n");
|
|
512
|
+
return successMessage;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// src/tools/searchCodeSmart.ts
|
|
517
|
+
import { z as z3 } from "zod";
|
|
518
|
+
|
|
519
|
+
// src/searcher.ts
|
|
520
|
+
import { SourceMapConsumer as SourceMapConsumer2 } from "source-map-js";
|
|
521
|
+
function createRegex(query, caseSensitive = false) {
|
|
522
|
+
try {
|
|
523
|
+
const flags = caseSensitive ? "g" : "gi";
|
|
524
|
+
return new RegExp(query, flags);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
527
|
+
throw new Error(`Invalid regex: ${message}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function getOriginalPosition(consumer, lineNumber) {
|
|
531
|
+
const pos = consumer.originalPositionFor({
|
|
532
|
+
line: lineNumber,
|
|
533
|
+
column: 0
|
|
534
|
+
});
|
|
535
|
+
return {
|
|
536
|
+
line: pos.line,
|
|
537
|
+
column: pos.column
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function createContextLine(lineNumber, content, consumer) {
|
|
541
|
+
return {
|
|
542
|
+
lineNumber,
|
|
543
|
+
content,
|
|
544
|
+
originalPosition: getOriginalPosition(consumer, lineNumber)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function searchInCode(code, rawMap, options) {
|
|
548
|
+
const {
|
|
549
|
+
query,
|
|
550
|
+
contextLines = 2,
|
|
551
|
+
caseSensitive = false,
|
|
552
|
+
maxMatches = 50
|
|
553
|
+
} = options;
|
|
554
|
+
const regex = createRegex(query, caseSensitive);
|
|
555
|
+
const lines = code.split("\n");
|
|
556
|
+
const totalLines = lines.length;
|
|
557
|
+
const consumer = new SourceMapConsumer2({
|
|
215
558
|
...rawMap,
|
|
216
559
|
version: String(rawMap.version)
|
|
217
560
|
});
|
|
561
|
+
const matchingLineNumbers = [];
|
|
562
|
+
for (let i = 0; i < totalLines; i++) {
|
|
563
|
+
const line = lines[i];
|
|
564
|
+
regex.lastIndex = 0;
|
|
565
|
+
if (regex.test(line)) {
|
|
566
|
+
matchingLineNumbers.push(i + 1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const totalMatches = matchingLineNumbers.length;
|
|
570
|
+
const truncated = totalMatches > maxMatches;
|
|
571
|
+
const limitedLineNumbers = matchingLineNumbers.slice(0, maxMatches);
|
|
572
|
+
const matches = limitedLineNumbers.map((lineNumber) => {
|
|
573
|
+
const lineIndex = lineNumber - 1;
|
|
574
|
+
const lineContent = lines[lineIndex];
|
|
575
|
+
const contextBefore = [];
|
|
576
|
+
for (let i = Math.max(0, lineIndex - contextLines); i < lineIndex; i++) {
|
|
577
|
+
contextBefore.push(createContextLine(i + 1, lines[i], consumer));
|
|
578
|
+
}
|
|
579
|
+
const contextAfter = [];
|
|
580
|
+
for (let i = lineIndex + 1; i <= Math.min(totalLines - 1, lineIndex + contextLines); i++) {
|
|
581
|
+
contextAfter.push(createContextLine(i + 1, lines[i], consumer));
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
lineNumber,
|
|
585
|
+
lineContent,
|
|
586
|
+
originalPosition: getOriginalPosition(consumer, lineNumber),
|
|
587
|
+
contextBefore,
|
|
588
|
+
contextAfter
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
return {
|
|
592
|
+
matches,
|
|
593
|
+
totalMatches,
|
|
594
|
+
truncated
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function formatSourcePosition2(line, column) {
|
|
598
|
+
if (line !== null && column !== null) {
|
|
599
|
+
return `L${line}:${column}`;
|
|
600
|
+
}
|
|
601
|
+
return "";
|
|
602
|
+
}
|
|
603
|
+
function formatSearchResult(filePath, query, caseSensitive, result, maxMatches = 50) {
|
|
604
|
+
const { matches, totalMatches, truncated } = result;
|
|
218
605
|
const outputParts = [];
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
606
|
+
const caseInfo = caseSensitive ? "case-sensitive" : "case-insensitive";
|
|
607
|
+
outputParts.push(`FILE: ${filePath}`);
|
|
608
|
+
outputParts.push(`QUERY: "${query}" (${caseInfo})`);
|
|
609
|
+
if (totalMatches === 0) {
|
|
610
|
+
outputParts.push("MATCHES: No matches found");
|
|
611
|
+
return outputParts.join("\n");
|
|
612
|
+
}
|
|
613
|
+
const matchInfo = truncated ? `MATCHES: ${totalMatches} found (showing first ${maxMatches})` : `MATCHES: ${totalMatches} found`;
|
|
614
|
+
outputParts.push(matchInfo);
|
|
615
|
+
outputParts.push("-".repeat(85));
|
|
616
|
+
for (const match of matches) {
|
|
617
|
+
outputParts.push(`--- Match at Line ${match.lineNumber} ---`);
|
|
618
|
+
const allLineNumbers = [
|
|
619
|
+
...match.contextBefore.map((c) => c.lineNumber),
|
|
620
|
+
match.lineNumber,
|
|
621
|
+
...match.contextAfter.map((c) => c.lineNumber)
|
|
622
|
+
];
|
|
623
|
+
const maxLineNumWidth = Math.max(...allLineNumbers.map((n) => String(n).length));
|
|
624
|
+
for (const ctx of match.contextBefore) {
|
|
625
|
+
const lineNumStr = String(ctx.lineNumber).padStart(maxLineNumWidth, " ");
|
|
626
|
+
const srcPos = formatSourcePosition2(ctx.originalPosition.line, ctx.originalPosition.column);
|
|
627
|
+
const srcPosPadded = srcPos ? `Src ${srcPos}` : "";
|
|
628
|
+
outputParts.push(` ${lineNumStr} | [${srcPosPadded.padEnd(14, " ")}] | ${ctx.content}`);
|
|
629
|
+
}
|
|
630
|
+
const matchLineNumStr = String(match.lineNumber).padStart(maxLineNumWidth, " ");
|
|
631
|
+
const matchSrcPos = formatSourcePosition2(match.originalPosition.line, match.originalPosition.column);
|
|
632
|
+
const matchSrcPosPadded = matchSrcPos ? `Src ${matchSrcPos}` : "";
|
|
633
|
+
outputParts.push(`>> ${matchLineNumStr} | [${matchSrcPosPadded.padEnd(14, " ")}] | ${match.lineContent}`);
|
|
634
|
+
for (const ctx of match.contextAfter) {
|
|
635
|
+
const lineNumStr = String(ctx.lineNumber).padStart(maxLineNumWidth, " ");
|
|
636
|
+
const srcPos = formatSourcePosition2(ctx.originalPosition.line, ctx.originalPosition.column);
|
|
637
|
+
const srcPosPadded = srcPos ? `Src ${srcPos}` : "";
|
|
638
|
+
outputParts.push(` ${lineNumStr} | [${srcPosPadded.padEnd(14, " ")}] | ${ctx.content}`);
|
|
639
|
+
}
|
|
640
|
+
outputParts.push("");
|
|
230
641
|
}
|
|
231
|
-
if (
|
|
232
|
-
outputParts.push(
|
|
642
|
+
if (truncated) {
|
|
643
|
+
outputParts.push(`... (${totalMatches - maxMatches} more matches not shown)`);
|
|
233
644
|
}
|
|
234
645
|
return outputParts.join("\n");
|
|
235
646
|
}
|
|
236
|
-
|
|
647
|
+
|
|
648
|
+
// src/tools/searchCodeSmart.ts
|
|
649
|
+
var SearchCodeSmartInputSchema = z3.object({
|
|
650
|
+
file_path: z3.string().describe("Path to the JavaScript file"),
|
|
651
|
+
query: z3.string().describe("Regex pattern or text to search"),
|
|
652
|
+
context_lines: z3.number().int().min(0).default(2).describe("Number of context lines"),
|
|
653
|
+
case_sensitive: z3.boolean().default(false).describe("Case sensitive search"),
|
|
654
|
+
char_limit: z3.number().int().min(50).default(300).describe("Character limit for string truncation"),
|
|
655
|
+
max_line_chars: z3.number().int().min(80).default(500).describe("Maximum characters per line")
|
|
656
|
+
});
|
|
657
|
+
var searchCodeSmart = defineTool({
|
|
658
|
+
name: "search_code_smart",
|
|
659
|
+
description: "Search for text or regex patterns in beautified JavaScript code. Returns matching lines with context and original source coordinates for setting breakpoints. Useful for finding code patterns in minified/obfuscated files.",
|
|
660
|
+
schema: {
|
|
661
|
+
file_path: z3.string().describe("Path to the JavaScript file"),
|
|
662
|
+
query: z3.string().describe("Regex pattern or text to search"),
|
|
663
|
+
context_lines: z3.number().int().min(0).default(2).describe("Number of context lines"),
|
|
664
|
+
case_sensitive: z3.boolean().default(false).describe("Case sensitive search"),
|
|
665
|
+
char_limit: z3.number().int().min(50).default(300).describe("Character limit for string truncation"),
|
|
666
|
+
max_line_chars: z3.number().int().min(80).default(500).describe("Maximum characters per line")
|
|
667
|
+
},
|
|
668
|
+
handler: async (params) => {
|
|
669
|
+
const { file_path, query, context_lines, case_sensitive, char_limit, max_line_chars } = params;
|
|
670
|
+
const beautifyResult = await ensureBeautified(file_path);
|
|
671
|
+
const { code, rawMap } = beautifyResult;
|
|
672
|
+
const truncatedCode = truncateCodeHighPerf(code, char_limit);
|
|
673
|
+
const searchResult = searchInCode(truncatedCode, rawMap, {
|
|
674
|
+
query,
|
|
675
|
+
contextLines: context_lines,
|
|
676
|
+
caseSensitive: case_sensitive,
|
|
677
|
+
maxMatches: 50
|
|
678
|
+
});
|
|
679
|
+
let output = formatSearchResult(file_path, query, case_sensitive, searchResult, 50);
|
|
680
|
+
output = truncateLongLines(output, max_line_chars);
|
|
681
|
+
return output;
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// src/tools/findUsageSmart.ts
|
|
686
|
+
import { z as z4 } from "zod";
|
|
687
|
+
|
|
688
|
+
// src/analyzer.ts
|
|
689
|
+
import { SourceMapConsumer as SourceMapConsumer3 } from "source-map-js";
|
|
690
|
+
import { parse as parse2 } from "@babel/parser";
|
|
691
|
+
var traverse = null;
|
|
692
|
+
async function getTraverse() {
|
|
693
|
+
if (!traverse) {
|
|
694
|
+
const mod = await import("@babel/traverse");
|
|
695
|
+
traverse = mod.default?.default ?? mod.default;
|
|
696
|
+
}
|
|
697
|
+
return traverse;
|
|
698
|
+
}
|
|
699
|
+
var DEFAULT_PARSER_OPTIONS = {
|
|
700
|
+
sourceType: "unambiguous",
|
|
701
|
+
plugins: [
|
|
702
|
+
"jsx",
|
|
703
|
+
"typescript",
|
|
704
|
+
"classProperties",
|
|
705
|
+
"classPrivateProperties",
|
|
706
|
+
"classPrivateMethods",
|
|
707
|
+
"dynamicImport",
|
|
708
|
+
"optionalChaining",
|
|
709
|
+
"nullishCoalescingOperator",
|
|
710
|
+
"objectRestSpread"
|
|
711
|
+
],
|
|
712
|
+
errorRecovery: true
|
|
713
|
+
};
|
|
714
|
+
function parseCode(code) {
|
|
715
|
+
try {
|
|
716
|
+
return parse2(code, DEFAULT_PARSER_OPTIONS);
|
|
717
|
+
} catch (err) {
|
|
718
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
719
|
+
throw new Error(`Parse error: ${message}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function getOriginalPosition2(consumer, line, column) {
|
|
723
|
+
const pos = consumer.originalPositionFor({ line, column });
|
|
724
|
+
return {
|
|
725
|
+
line: pos.line,
|
|
726
|
+
column: pos.column
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function getLineContent(lines, lineNumber) {
|
|
730
|
+
if (lineNumber < 1 || lineNumber > lines.length) {
|
|
731
|
+
return "";
|
|
732
|
+
}
|
|
733
|
+
return lines[lineNumber - 1];
|
|
734
|
+
}
|
|
735
|
+
function createLocationInfo(line, column, lines, consumer) {
|
|
237
736
|
return {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
737
|
+
line,
|
|
738
|
+
column,
|
|
739
|
+
originalPosition: getOriginalPosition2(consumer, line, column),
|
|
740
|
+
lineContent: getLineContent(lines, line)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function analyzeBindings(code, rawMap, identifier, options) {
|
|
744
|
+
const targetLine = options?.targetLine;
|
|
745
|
+
const isTargeted = targetLine !== void 0;
|
|
746
|
+
const maxReferences = options?.maxReferences ?? (isTargeted ? 15 : 10);
|
|
747
|
+
const ast = parseCode(code);
|
|
748
|
+
const lines = code.split("\n");
|
|
749
|
+
const consumer = new SourceMapConsumer3({
|
|
750
|
+
...rawMap,
|
|
751
|
+
version: String(rawMap.version)
|
|
752
|
+
});
|
|
753
|
+
const bindings = [];
|
|
754
|
+
const processedScopes = /* @__PURE__ */ new Set();
|
|
755
|
+
const traverse2 = await getTraverse();
|
|
756
|
+
try {
|
|
757
|
+
traverse2(ast, {
|
|
758
|
+
Identifier(path3) {
|
|
759
|
+
if (path3.node.name !== identifier) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (isTargeted) {
|
|
763
|
+
const nodeLoc = path3.node.loc;
|
|
764
|
+
if (!nodeLoc || nodeLoc.start.line !== targetLine) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const binding = path3.scope.getBinding(identifier);
|
|
769
|
+
if (!binding) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const scopeUid = binding.scope.uid;
|
|
773
|
+
if (processedScopes.has(scopeUid)) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
processedScopes.add(scopeUid);
|
|
777
|
+
const defNode = binding.identifier;
|
|
778
|
+
const defLoc = defNode.loc;
|
|
779
|
+
if (!defLoc) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const definition = createLocationInfo(
|
|
783
|
+
defLoc.start.line,
|
|
784
|
+
defLoc.start.column,
|
|
785
|
+
lines,
|
|
786
|
+
consumer
|
|
787
|
+
);
|
|
788
|
+
const allReferences = [];
|
|
789
|
+
for (const refPath of binding.referencePaths) {
|
|
790
|
+
const refLoc = refPath.node.loc;
|
|
791
|
+
if (!refLoc) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
allReferences.push(
|
|
795
|
+
createLocationInfo(
|
|
796
|
+
refLoc.start.line,
|
|
797
|
+
refLoc.start.column,
|
|
798
|
+
lines,
|
|
799
|
+
consumer
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const totalReferences = allReferences.length;
|
|
804
|
+
const limitedReferences = allReferences.slice(0, maxReferences);
|
|
805
|
+
let hitLocation;
|
|
806
|
+
if (isTargeted) {
|
|
807
|
+
const nodeLoc = path3.node.loc;
|
|
808
|
+
hitLocation = createLocationInfo(
|
|
809
|
+
nodeLoc.start.line,
|
|
810
|
+
nodeLoc.start.column,
|
|
811
|
+
lines,
|
|
812
|
+
consumer
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
bindings.push({
|
|
816
|
+
scopeUid,
|
|
817
|
+
kind: binding.kind,
|
|
818
|
+
definition,
|
|
819
|
+
references: limitedReferences,
|
|
820
|
+
totalReferences,
|
|
821
|
+
hitLocation
|
|
822
|
+
});
|
|
823
|
+
if (isTargeted) {
|
|
824
|
+
path3.stop();
|
|
264
825
|
}
|
|
265
826
|
}
|
|
266
|
-
|
|
827
|
+
});
|
|
828
|
+
} catch (err) {
|
|
829
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
830
|
+
throw new Error(`Analysis error: ${message}`);
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
bindings,
|
|
834
|
+
identifier,
|
|
835
|
+
isTargeted,
|
|
836
|
+
targetLine
|
|
267
837
|
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
throw new Error(`Tool not found: ${name}`);
|
|
838
|
+
}
|
|
839
|
+
function formatSourcePosition3(line, column) {
|
|
840
|
+
if (line !== null && column !== null) {
|
|
841
|
+
return `L${line}:${column}`;
|
|
273
842
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
843
|
+
return "";
|
|
844
|
+
}
|
|
845
|
+
function locationsMatch(loc1, loc2) {
|
|
846
|
+
return loc1.line === loc2.line && loc1.column === loc2.column;
|
|
847
|
+
}
|
|
848
|
+
function formatAnalysisResult(filePath, result, maxReferences = 10) {
|
|
849
|
+
const { bindings, identifier, isTargeted, targetLine } = result;
|
|
850
|
+
const outputParts = [];
|
|
851
|
+
outputParts.push(`FILE: ${filePath}`);
|
|
852
|
+
outputParts.push(`IDENTIFIER: "${identifier}"`);
|
|
853
|
+
if (bindings.length === 0) {
|
|
854
|
+
if (isTargeted && targetLine !== void 0) {
|
|
855
|
+
outputParts.push(`BINDINGS: No binding found for "${identifier}" at line ${targetLine}`);
|
|
856
|
+
outputParts.push(`The variable may be global, externally defined, or not present at this line.`);
|
|
857
|
+
} else {
|
|
858
|
+
outputParts.push("BINDINGS: No definitions or references found");
|
|
859
|
+
}
|
|
860
|
+
return outputParts.join("\n");
|
|
861
|
+
}
|
|
862
|
+
if (isTargeted) {
|
|
863
|
+
outputParts.push(`BINDINGS: 1 found (Targeted Scope at line ${targetLine})`);
|
|
864
|
+
} else {
|
|
865
|
+
const scopeInfo = bindings.length > 1 ? " (in different scopes)" : "";
|
|
866
|
+
outputParts.push(`BINDINGS: ${bindings.length} found${scopeInfo}`);
|
|
867
|
+
}
|
|
868
|
+
outputParts.push("-".repeat(85));
|
|
869
|
+
for (let i = 0; i < bindings.length; i++) {
|
|
870
|
+
const binding = bindings[i];
|
|
871
|
+
if (isTargeted) {
|
|
872
|
+
outputParts.push(`=== Targeted Scope (${binding.kind}) ===`);
|
|
873
|
+
} else {
|
|
874
|
+
outputParts.push(`=== Scope #${i + 1} (${binding.kind}) ===`);
|
|
875
|
+
}
|
|
876
|
+
const defIsHit = isTargeted && binding.hitLocation && locationsMatch(binding.definition, binding.hitLocation);
|
|
877
|
+
const defPrefix = defIsHit ? "\u{1F4CD} Definition (hit):" : "\u{1F4CD} Definition:";
|
|
878
|
+
outputParts.push(defPrefix);
|
|
879
|
+
const defSrcPos = formatSourcePosition3(
|
|
880
|
+
binding.definition.originalPosition.line,
|
|
881
|
+
binding.definition.originalPosition.column
|
|
882
|
+
);
|
|
883
|
+
const defSrcPosPadded = defSrcPos ? `Src ${defSrcPos}` : "";
|
|
884
|
+
const defMarker = defIsHit ? " \u25C0\u2500\u2500 hit" : "";
|
|
885
|
+
outputParts.push(
|
|
886
|
+
` ${binding.definition.line} | [${defSrcPosPadded.padEnd(14, " ")}] | ${binding.definition.lineContent}${defMarker}`
|
|
887
|
+
);
|
|
888
|
+
const totalRefs = binding.totalReferences;
|
|
889
|
+
if (totalRefs === 0) {
|
|
890
|
+
outputParts.push("\u{1F50E} References: None");
|
|
891
|
+
} else {
|
|
892
|
+
outputParts.push(`\u{1F50E} References (${totalRefs}):`);
|
|
893
|
+
for (const ref of binding.references) {
|
|
894
|
+
const refIsHit = isTargeted && binding.hitLocation && locationsMatch(ref, binding.hitLocation);
|
|
895
|
+
const refSrcPos = formatSourcePosition3(
|
|
896
|
+
ref.originalPosition.line,
|
|
897
|
+
ref.originalPosition.column
|
|
898
|
+
);
|
|
899
|
+
const refSrcPosPadded = refSrcPos ? `Src ${refSrcPos}` : "";
|
|
900
|
+
const refMarker = refIsHit ? " \u25C0\u2500\u2500 hit" : "";
|
|
901
|
+
outputParts.push(
|
|
902
|
+
` ${ref.line} | [${refSrcPosPadded.padEnd(14, " ")}] | ${ref.lineContent}${refMarker}`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (totalRefs > maxReferences) {
|
|
906
|
+
const remaining = totalRefs - maxReferences;
|
|
907
|
+
outputParts.push(` ... (${remaining} more references not shown)`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
outputParts.push("");
|
|
911
|
+
}
|
|
912
|
+
return outputParts.join("\n");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/tools/findUsageSmart.ts
|
|
916
|
+
var FindUsageSmartInputSchema = z4.object({
|
|
917
|
+
file_path: z4.string().describe("Path to the JavaScript file"),
|
|
918
|
+
identifier: z4.string().describe("Variable or function name to find"),
|
|
919
|
+
line: z4.number().int().positive().optional().describe(
|
|
920
|
+
"The line number where you see this variable. HIGHLY RECOMMENDED for precision in obfuscated code."
|
|
921
|
+
),
|
|
922
|
+
char_limit: z4.number().int().min(50).default(300).describe("Character limit for string truncation"),
|
|
923
|
+
max_line_chars: z4.number().int().min(80).default(500).describe("Maximum characters per line")
|
|
924
|
+
});
|
|
925
|
+
var findUsageSmart = defineTool({
|
|
926
|
+
name: "find_usage_smart",
|
|
927
|
+
description: "Find all definitions and references of a variable/function using AST scope analysis. Returns binding information grouped by scope with original source coordinates for setting breakpoints. Useful for tracing variable usage in minified/obfuscated code with variable name reuse.",
|
|
928
|
+
schema: {
|
|
929
|
+
file_path: z4.string().describe("Path to the JavaScript file"),
|
|
930
|
+
identifier: z4.string().describe("Variable or function name to find"),
|
|
931
|
+
line: z4.number().int().positive().optional().describe(
|
|
932
|
+
"The line number where you see this variable. HIGHLY RECOMMENDED for precision in obfuscated code."
|
|
933
|
+
),
|
|
934
|
+
char_limit: z4.number().int().min(50).default(300).describe("Character limit for string truncation"),
|
|
935
|
+
max_line_chars: z4.number().int().min(80).default(500).describe("Maximum characters per line")
|
|
936
|
+
},
|
|
937
|
+
handler: async (params) => {
|
|
938
|
+
const { file_path, identifier, line, char_limit, max_line_chars } = params;
|
|
939
|
+
const beautifyResult = await ensureBeautified(file_path);
|
|
940
|
+
const { code, rawMap } = beautifyResult;
|
|
941
|
+
const analysisResult = await analyzeBindings(code, rawMap, identifier, {
|
|
942
|
+
maxReferences: line ? 15 : 10,
|
|
943
|
+
targetLine: line
|
|
944
|
+
});
|
|
945
|
+
const truncatedCode = truncateCodeHighPerf(code, char_limit);
|
|
946
|
+
const truncatedLines = truncatedCode.split("\n");
|
|
947
|
+
for (const binding of analysisResult.bindings) {
|
|
948
|
+
const defLineIdx = binding.definition.line - 1;
|
|
949
|
+
if (defLineIdx >= 0 && defLineIdx < truncatedLines.length) {
|
|
950
|
+
binding.definition.lineContent = truncatedLines[defLineIdx];
|
|
951
|
+
}
|
|
952
|
+
for (const ref of binding.references) {
|
|
953
|
+
const refLineIdx = ref.line - 1;
|
|
954
|
+
if (refLineIdx >= 0 && refLineIdx < truncatedLines.length) {
|
|
955
|
+
ref.lineContent = truncatedLines[refLineIdx];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
let output = formatAnalysisResult(file_path, analysisResult, line ? 15 : 10);
|
|
960
|
+
output = truncateLongLines(output, max_line_chars);
|
|
961
|
+
return output;
|
|
286
962
|
}
|
|
287
963
|
});
|
|
964
|
+
|
|
965
|
+
// src/tools/index.ts
|
|
966
|
+
var tools = [
|
|
967
|
+
readCodeSmart,
|
|
968
|
+
applyCustomTransform2,
|
|
969
|
+
searchCodeSmart,
|
|
970
|
+
findUsageSmart
|
|
971
|
+
];
|
|
972
|
+
|
|
973
|
+
// src/server.ts
|
|
974
|
+
var server = new McpServer({
|
|
975
|
+
name: "smart-fs",
|
|
976
|
+
version: "1.0.0"
|
|
977
|
+
});
|
|
978
|
+
function registerTool(tool) {
|
|
979
|
+
const zodSchema = z5.object(tool.schema);
|
|
980
|
+
server.registerTool(
|
|
981
|
+
tool.name,
|
|
982
|
+
{
|
|
983
|
+
description: tool.description,
|
|
984
|
+
inputSchema: tool.schema
|
|
985
|
+
},
|
|
986
|
+
async (params, _extra) => {
|
|
987
|
+
try {
|
|
988
|
+
const validatedParams = zodSchema.parse(params);
|
|
989
|
+
const result = await tool.handler(validatedParams);
|
|
990
|
+
return {
|
|
991
|
+
content: [{ type: "text", text: result }]
|
|
992
|
+
};
|
|
993
|
+
} catch (error) {
|
|
994
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
995
|
+
return {
|
|
996
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
997
|
+
isError: true
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
for (const tool of tools) {
|
|
1004
|
+
registerTool(tool);
|
|
1005
|
+
}
|
|
288
1006
|
async function main() {
|
|
289
1007
|
const transport = new StdioServerTransport();
|
|
290
1008
|
await server.connect(transport);
|
|
291
|
-
console.error("
|
|
1009
|
+
console.error("Smart FS MCP Server running on stdio");
|
|
292
1010
|
}
|
|
293
1011
|
main().catch((error) => {
|
|
294
1012
|
console.error("Fatal error:", error);
|