@rog0x/mcp-file-tools 1.0.0
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 +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +209 -0
- package/dist/tools/code-counter.d.ts +22 -0
- package/dist/tools/code-counter.js +187 -0
- package/dist/tools/dir-tree.d.ts +12 -0
- package/dist/tools/dir-tree.js +107 -0
- package/dist/tools/duplicate-finder.d.ts +22 -0
- package/dist/tools/duplicate-finder.js +143 -0
- package/dist/tools/file-search.d.ts +36 -0
- package/dist/tools/file-search.js +146 -0
- package/dist/tools/file-stats.d.ts +25 -0
- package/dist/tools/file-stats.js +128 -0
- package/package.json +37 -0
- package/src/index.ts +230 -0
- package/src/tools/code-counter.ts +208 -0
- package/src/tools/dir-tree.ts +108 -0
- package/src/tools/duplicate-finder.ts +145 -0
- package/src/tools/file-search.ts +157 -0
- package/src/tools/file-stats.ts +128 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
|
|
5
|
+
interface FileSearchOptions {
|
|
6
|
+
dirPath: string;
|
|
7
|
+
namePattern?: string;
|
|
8
|
+
contentPattern?: string;
|
|
9
|
+
minSize?: number;
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
modifiedAfter?: string;
|
|
12
|
+
modifiedBefore?: string;
|
|
13
|
+
maxResults?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SearchMatch {
|
|
17
|
+
path: string;
|
|
18
|
+
size: string;
|
|
19
|
+
sizeBytes: number;
|
|
20
|
+
modified: string;
|
|
21
|
+
contentMatches?: { line: number; text: string }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FileSearchResult {
|
|
25
|
+
directory: string;
|
|
26
|
+
query: {
|
|
27
|
+
namePattern?: string;
|
|
28
|
+
contentPattern?: string;
|
|
29
|
+
minSize?: number;
|
|
30
|
+
maxSize?: number;
|
|
31
|
+
modifiedAfter?: string;
|
|
32
|
+
modifiedBefore?: string;
|
|
33
|
+
};
|
|
34
|
+
totalMatches: number;
|
|
35
|
+
matches: SearchMatch[];
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatSize(bytes: number): string {
|
|
40
|
+
if (bytes === 0) return "0 B";
|
|
41
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
42
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
43
|
+
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function fileSearch(options: FileSearchOptions): Promise<FileSearchResult> {
|
|
47
|
+
const {
|
|
48
|
+
dirPath,
|
|
49
|
+
namePattern,
|
|
50
|
+
contentPattern,
|
|
51
|
+
minSize,
|
|
52
|
+
maxSize,
|
|
53
|
+
modifiedAfter,
|
|
54
|
+
modifiedBefore,
|
|
55
|
+
maxResults = 100,
|
|
56
|
+
} = options;
|
|
57
|
+
|
|
58
|
+
const resolvedPath = path.resolve(dirPath);
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
61
|
+
throw new Error(`Directory not found: ${resolvedPath}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Find files by name pattern using glob
|
|
65
|
+
const globPattern = namePattern || "**/*";
|
|
66
|
+
const files = await glob(globPattern, {
|
|
67
|
+
cwd: resolvedPath,
|
|
68
|
+
nodir: true,
|
|
69
|
+
absolute: true,
|
|
70
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/__pycache__/**"],
|
|
71
|
+
dot: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const matches: SearchMatch[] = [];
|
|
75
|
+
const modAfterDate = modifiedAfter ? new Date(modifiedAfter) : null;
|
|
76
|
+
const modBeforeDate = modifiedBefore ? new Date(modifiedBefore) : null;
|
|
77
|
+
let contentRegex: RegExp | null = null;
|
|
78
|
+
if (contentPattern) {
|
|
79
|
+
try {
|
|
80
|
+
contentRegex = new RegExp(contentPattern, "gi");
|
|
81
|
+
} catch (e: unknown) {
|
|
82
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
83
|
+
throw new Error(`Invalid content regex: ${msg}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const filePath of files) {
|
|
88
|
+
if (matches.length >= maxResults) break;
|
|
89
|
+
|
|
90
|
+
let stat: fs.Stats;
|
|
91
|
+
try {
|
|
92
|
+
stat = fs.statSync(filePath);
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Size filters
|
|
98
|
+
if (minSize !== undefined && stat.size < minSize) continue;
|
|
99
|
+
if (maxSize !== undefined && stat.size > maxSize) continue;
|
|
100
|
+
|
|
101
|
+
// Date filters
|
|
102
|
+
if (modAfterDate && stat.mtime < modAfterDate) continue;
|
|
103
|
+
if (modBeforeDate && stat.mtime > modBeforeDate) continue;
|
|
104
|
+
|
|
105
|
+
// Content search
|
|
106
|
+
let contentMatches: { line: number; text: string }[] | undefined;
|
|
107
|
+
if (contentRegex) {
|
|
108
|
+
// Skip binary files and very large files
|
|
109
|
+
if (stat.size > 10 * 1024 * 1024) continue;
|
|
110
|
+
|
|
111
|
+
let content: string;
|
|
112
|
+
try {
|
|
113
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Quick binary check
|
|
119
|
+
if (content.includes("\0")) continue;
|
|
120
|
+
|
|
121
|
+
const lines = content.split(/\r?\n/);
|
|
122
|
+
contentMatches = [];
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
contentRegex.lastIndex = 0;
|
|
125
|
+
if (contentRegex.test(lines[i])) {
|
|
126
|
+
contentMatches.push({ line: i + 1, text: lines[i].trim().substring(0, 200) });
|
|
127
|
+
if (contentMatches.length >= 20) break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (contentMatches.length === 0) continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
matches.push({
|
|
135
|
+
path: filePath,
|
|
136
|
+
size: formatSize(stat.size),
|
|
137
|
+
sizeBytes: stat.size,
|
|
138
|
+
modified: stat.mtime.toISOString(),
|
|
139
|
+
...(contentMatches ? { contentMatches } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
directory: resolvedPath,
|
|
145
|
+
query: {
|
|
146
|
+
...(namePattern ? { namePattern } : {}),
|
|
147
|
+
...(contentPattern ? { contentPattern } : {}),
|
|
148
|
+
...(minSize !== undefined ? { minSize } : {}),
|
|
149
|
+
...(maxSize !== undefined ? { maxSize } : {}),
|
|
150
|
+
...(modifiedAfter ? { modifiedAfter } : {}),
|
|
151
|
+
...(modifiedBefore ? { modifiedBefore } : {}),
|
|
152
|
+
},
|
|
153
|
+
totalMatches: matches.length,
|
|
154
|
+
matches,
|
|
155
|
+
truncated: matches.length >= maxResults,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
interface FileStatsOptions {
|
|
5
|
+
dirPath: string;
|
|
6
|
+
maxDepth?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface FileInfo {
|
|
10
|
+
path: string;
|
|
11
|
+
size: number;
|
|
12
|
+
modified: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FileStatsResult {
|
|
16
|
+
directory: string;
|
|
17
|
+
totalFiles: number;
|
|
18
|
+
totalSize: string;
|
|
19
|
+
totalSizeBytes: number;
|
|
20
|
+
extensionCounts: Record<string, { count: number; totalSize: string; totalSizeBytes: number }>;
|
|
21
|
+
largestFiles: FileInfo[];
|
|
22
|
+
newestFiles: FileInfo[];
|
|
23
|
+
oldestFiles: FileInfo[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatSize(bytes: number): string {
|
|
27
|
+
if (bytes === 0) return "0 B";
|
|
28
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
29
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
30
|
+
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectFiles(
|
|
34
|
+
dirPath: string,
|
|
35
|
+
depth: number,
|
|
36
|
+
maxDepth: number,
|
|
37
|
+
files: FileInfo[]
|
|
38
|
+
): void {
|
|
39
|
+
if (maxDepth > 0 && depth >= maxDepth) return;
|
|
40
|
+
|
|
41
|
+
let entries: fs.Dirent[];
|
|
42
|
+
try {
|
|
43
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
50
|
+
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (["node_modules", ".git", "dist", "__pycache__"].includes(entry.name)) continue;
|
|
53
|
+
collectFiles(fullPath, depth + 1, maxDepth, files);
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
try {
|
|
56
|
+
const stat = fs.statSync(fullPath);
|
|
57
|
+
files.push({
|
|
58
|
+
path: fullPath,
|
|
59
|
+
size: stat.size,
|
|
60
|
+
modified: stat.mtime.toISOString(),
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// Skip inaccessible files
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function fileStats(options: FileStatsOptions): Promise<FileStatsResult> {
|
|
70
|
+
const { dirPath, maxDepth = 10 } = options;
|
|
71
|
+
const resolvedPath = path.resolve(dirPath);
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
74
|
+
throw new Error(`Directory not found: ${resolvedPath}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const files: FileInfo[] = [];
|
|
78
|
+
collectFiles(resolvedPath, 0, maxDepth, files);
|
|
79
|
+
|
|
80
|
+
const totalSizeBytes = files.reduce((sum, f) => sum + f.size, 0);
|
|
81
|
+
|
|
82
|
+
// Count by extension
|
|
83
|
+
const extMap: Record<string, { count: number; totalSizeBytes: number }> = {};
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
const ext = path.extname(file.path).toLowerCase() || "(no extension)";
|
|
86
|
+
if (!extMap[ext]) {
|
|
87
|
+
extMap[ext] = { count: 0, totalSizeBytes: 0 };
|
|
88
|
+
}
|
|
89
|
+
extMap[ext].count++;
|
|
90
|
+
extMap[ext].totalSizeBytes += file.size;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const extensionCounts: Record<string, { count: number; totalSize: string; totalSizeBytes: number }> = {};
|
|
94
|
+
const sortedExts = Object.entries(extMap).sort((a, b) => b[1].count - a[1].count);
|
|
95
|
+
for (const [ext, data] of sortedExts) {
|
|
96
|
+
extensionCounts[ext] = {
|
|
97
|
+
count: data.count,
|
|
98
|
+
totalSize: formatSize(data.totalSizeBytes),
|
|
99
|
+
totalSizeBytes: data.totalSizeBytes,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Largest files (top 10)
|
|
104
|
+
const largestFiles = [...files]
|
|
105
|
+
.sort((a, b) => b.size - a.size)
|
|
106
|
+
.slice(0, 10);
|
|
107
|
+
|
|
108
|
+
// Newest files (top 10)
|
|
109
|
+
const newestFiles = [...files]
|
|
110
|
+
.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime())
|
|
111
|
+
.slice(0, 10);
|
|
112
|
+
|
|
113
|
+
// Oldest files (top 10)
|
|
114
|
+
const oldestFiles = [...files]
|
|
115
|
+
.sort((a, b) => new Date(a.modified).getTime() - new Date(b.modified).getTime())
|
|
116
|
+
.slice(0, 10);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
directory: resolvedPath,
|
|
120
|
+
totalFiles: files.length,
|
|
121
|
+
totalSize: formatSize(totalSizeBytes),
|
|
122
|
+
totalSizeBytes,
|
|
123
|
+
extensionCounts,
|
|
124
|
+
largestFiles,
|
|
125
|
+
newestFiles,
|
|
126
|
+
oldestFiles,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|