@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,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.duplicateFinder = duplicateFinder;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
function formatSize(bytes) {
|
|
41
|
+
if (bytes === 0)
|
|
42
|
+
return "0 B";
|
|
43
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
44
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
45
|
+
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
|
|
46
|
+
}
|
|
47
|
+
function collectFiles(dirPath, depth, maxDepth, files) {
|
|
48
|
+
if (maxDepth > 0 && depth >= maxDepth)
|
|
49
|
+
return;
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
if (["node_modules", ".git", "dist", "__pycache__"].includes(entry.name))
|
|
61
|
+
continue;
|
|
62
|
+
collectFiles(fullPath, depth + 1, maxDepth, files);
|
|
63
|
+
}
|
|
64
|
+
else if (entry.isFile()) {
|
|
65
|
+
try {
|
|
66
|
+
const stat = fs.statSync(fullPath);
|
|
67
|
+
files.push({ path: fullPath, size: stat.size });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Skip inaccessible files
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function hashFile(filePath) {
|
|
76
|
+
const content = fs.readFileSync(filePath);
|
|
77
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
async function duplicateFinder(options) {
|
|
80
|
+
const { dirPath, minSize = 1, maxDepth = 10 } = options;
|
|
81
|
+
const resolvedPath = path.resolve(dirPath);
|
|
82
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
83
|
+
throw new Error(`Directory not found: ${resolvedPath}`);
|
|
84
|
+
}
|
|
85
|
+
const allFiles = [];
|
|
86
|
+
collectFiles(resolvedPath, 0, maxDepth, allFiles);
|
|
87
|
+
// Filter by minimum size
|
|
88
|
+
const filteredFiles = allFiles.filter((f) => f.size >= minSize);
|
|
89
|
+
// Group by size first (optimization: only hash files with matching sizes)
|
|
90
|
+
const sizeGroups = {};
|
|
91
|
+
for (const file of filteredFiles) {
|
|
92
|
+
if (!sizeGroups[file.size]) {
|
|
93
|
+
sizeGroups[file.size] = [];
|
|
94
|
+
}
|
|
95
|
+
sizeGroups[file.size].push(file);
|
|
96
|
+
}
|
|
97
|
+
// Hash only files that share a size
|
|
98
|
+
const hashGroups = {};
|
|
99
|
+
for (const [, group] of Object.entries(sizeGroups)) {
|
|
100
|
+
if (group.length < 2)
|
|
101
|
+
continue;
|
|
102
|
+
for (const file of group) {
|
|
103
|
+
try {
|
|
104
|
+
const hash = hashFile(file.path);
|
|
105
|
+
if (!hashGroups[hash]) {
|
|
106
|
+
hashGroups[hash] = [];
|
|
107
|
+
}
|
|
108
|
+
hashGroups[hash].push(file);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Skip files that can't be read
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Build result groups (only actual duplicates)
|
|
116
|
+
const groups = [];
|
|
117
|
+
let totalDuplicateFiles = 0;
|
|
118
|
+
let wastedSpaceBytes = 0;
|
|
119
|
+
for (const [hash, files] of Object.entries(hashGroups)) {
|
|
120
|
+
if (files.length < 2)
|
|
121
|
+
continue;
|
|
122
|
+
const sizeBytes = files[0].size;
|
|
123
|
+
groups.push({
|
|
124
|
+
hash,
|
|
125
|
+
size: formatSize(sizeBytes),
|
|
126
|
+
sizeBytes,
|
|
127
|
+
files: files.map((f) => f.path),
|
|
128
|
+
});
|
|
129
|
+
totalDuplicateFiles += files.length;
|
|
130
|
+
wastedSpaceBytes += sizeBytes * (files.length - 1);
|
|
131
|
+
}
|
|
132
|
+
// Sort by wasted space descending
|
|
133
|
+
groups.sort((a, b) => b.sizeBytes * (b.files.length - 1) - a.sizeBytes * (a.files.length - 1));
|
|
134
|
+
return {
|
|
135
|
+
directory: resolvedPath,
|
|
136
|
+
totalFilesScanned: filteredFiles.length,
|
|
137
|
+
duplicateGroups: groups.length,
|
|
138
|
+
totalDuplicateFiles,
|
|
139
|
+
wastedSpace: formatSize(wastedSpaceBytes),
|
|
140
|
+
wastedSpaceBytes,
|
|
141
|
+
groups: groups.slice(0, 50),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface FileSearchOptions {
|
|
2
|
+
dirPath: string;
|
|
3
|
+
namePattern?: string;
|
|
4
|
+
contentPattern?: string;
|
|
5
|
+
minSize?: number;
|
|
6
|
+
maxSize?: number;
|
|
7
|
+
modifiedAfter?: string;
|
|
8
|
+
modifiedBefore?: string;
|
|
9
|
+
maxResults?: number;
|
|
10
|
+
}
|
|
11
|
+
interface SearchMatch {
|
|
12
|
+
path: string;
|
|
13
|
+
size: string;
|
|
14
|
+
sizeBytes: number;
|
|
15
|
+
modified: string;
|
|
16
|
+
contentMatches?: {
|
|
17
|
+
line: number;
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
interface FileSearchResult {
|
|
22
|
+
directory: string;
|
|
23
|
+
query: {
|
|
24
|
+
namePattern?: string;
|
|
25
|
+
contentPattern?: string;
|
|
26
|
+
minSize?: number;
|
|
27
|
+
maxSize?: number;
|
|
28
|
+
modifiedAfter?: string;
|
|
29
|
+
modifiedBefore?: string;
|
|
30
|
+
};
|
|
31
|
+
totalMatches: number;
|
|
32
|
+
matches: SearchMatch[];
|
|
33
|
+
truncated: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function fileSearch(options: FileSearchOptions): Promise<FileSearchResult>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.fileSearch = fileSearch;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const glob_1 = require("glob");
|
|
40
|
+
function formatSize(bytes) {
|
|
41
|
+
if (bytes === 0)
|
|
42
|
+
return "0 B";
|
|
43
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
44
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
45
|
+
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
|
|
46
|
+
}
|
|
47
|
+
async function fileSearch(options) {
|
|
48
|
+
const { dirPath, namePattern, contentPattern, minSize, maxSize, modifiedAfter, modifiedBefore, maxResults = 100, } = options;
|
|
49
|
+
const resolvedPath = path.resolve(dirPath);
|
|
50
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
51
|
+
throw new Error(`Directory not found: ${resolvedPath}`);
|
|
52
|
+
}
|
|
53
|
+
// Find files by name pattern using glob
|
|
54
|
+
const globPattern = namePattern || "**/*";
|
|
55
|
+
const files = await (0, glob_1.glob)(globPattern, {
|
|
56
|
+
cwd: resolvedPath,
|
|
57
|
+
nodir: true,
|
|
58
|
+
absolute: true,
|
|
59
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/__pycache__/**"],
|
|
60
|
+
dot: false,
|
|
61
|
+
});
|
|
62
|
+
const matches = [];
|
|
63
|
+
const modAfterDate = modifiedAfter ? new Date(modifiedAfter) : null;
|
|
64
|
+
const modBeforeDate = modifiedBefore ? new Date(modifiedBefore) : null;
|
|
65
|
+
let contentRegex = null;
|
|
66
|
+
if (contentPattern) {
|
|
67
|
+
try {
|
|
68
|
+
contentRegex = new RegExp(contentPattern, "gi");
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
72
|
+
throw new Error(`Invalid content regex: ${msg}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const filePath of files) {
|
|
76
|
+
if (matches.length >= maxResults)
|
|
77
|
+
break;
|
|
78
|
+
let stat;
|
|
79
|
+
try {
|
|
80
|
+
stat = fs.statSync(filePath);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Size filters
|
|
86
|
+
if (minSize !== undefined && stat.size < minSize)
|
|
87
|
+
continue;
|
|
88
|
+
if (maxSize !== undefined && stat.size > maxSize)
|
|
89
|
+
continue;
|
|
90
|
+
// Date filters
|
|
91
|
+
if (modAfterDate && stat.mtime < modAfterDate)
|
|
92
|
+
continue;
|
|
93
|
+
if (modBeforeDate && stat.mtime > modBeforeDate)
|
|
94
|
+
continue;
|
|
95
|
+
// Content search
|
|
96
|
+
let contentMatches;
|
|
97
|
+
if (contentRegex) {
|
|
98
|
+
// Skip binary files and very large files
|
|
99
|
+
if (stat.size > 10 * 1024 * 1024)
|
|
100
|
+
continue;
|
|
101
|
+
let content;
|
|
102
|
+
try {
|
|
103
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Quick binary check
|
|
109
|
+
if (content.includes("\0"))
|
|
110
|
+
continue;
|
|
111
|
+
const lines = content.split(/\r?\n/);
|
|
112
|
+
contentMatches = [];
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
contentRegex.lastIndex = 0;
|
|
115
|
+
if (contentRegex.test(lines[i])) {
|
|
116
|
+
contentMatches.push({ line: i + 1, text: lines[i].trim().substring(0, 200) });
|
|
117
|
+
if (contentMatches.length >= 20)
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (contentMatches.length === 0)
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
matches.push({
|
|
125
|
+
path: filePath,
|
|
126
|
+
size: formatSize(stat.size),
|
|
127
|
+
sizeBytes: stat.size,
|
|
128
|
+
modified: stat.mtime.toISOString(),
|
|
129
|
+
...(contentMatches ? { contentMatches } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
directory: resolvedPath,
|
|
134
|
+
query: {
|
|
135
|
+
...(namePattern ? { namePattern } : {}),
|
|
136
|
+
...(contentPattern ? { contentPattern } : {}),
|
|
137
|
+
...(minSize !== undefined ? { minSize } : {}),
|
|
138
|
+
...(maxSize !== undefined ? { maxSize } : {}),
|
|
139
|
+
...(modifiedAfter ? { modifiedAfter } : {}),
|
|
140
|
+
...(modifiedBefore ? { modifiedBefore } : {}),
|
|
141
|
+
},
|
|
142
|
+
totalMatches: matches.length,
|
|
143
|
+
matches,
|
|
144
|
+
truncated: matches.length >= maxResults,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface FileStatsOptions {
|
|
2
|
+
dirPath: string;
|
|
3
|
+
maxDepth?: number;
|
|
4
|
+
}
|
|
5
|
+
interface FileInfo {
|
|
6
|
+
path: string;
|
|
7
|
+
size: number;
|
|
8
|
+
modified: string;
|
|
9
|
+
}
|
|
10
|
+
interface FileStatsResult {
|
|
11
|
+
directory: string;
|
|
12
|
+
totalFiles: number;
|
|
13
|
+
totalSize: string;
|
|
14
|
+
totalSizeBytes: number;
|
|
15
|
+
extensionCounts: Record<string, {
|
|
16
|
+
count: number;
|
|
17
|
+
totalSize: string;
|
|
18
|
+
totalSizeBytes: number;
|
|
19
|
+
}>;
|
|
20
|
+
largestFiles: FileInfo[];
|
|
21
|
+
newestFiles: FileInfo[];
|
|
22
|
+
oldestFiles: FileInfo[];
|
|
23
|
+
}
|
|
24
|
+
export declare function fileStats(options: FileStatsOptions): Promise<FileStatsResult>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.fileStats = fileStats;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
function formatSize(bytes) {
|
|
40
|
+
if (bytes === 0)
|
|
41
|
+
return "0 B";
|
|
42
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
43
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
44
|
+
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
|
|
45
|
+
}
|
|
46
|
+
function collectFiles(dirPath, depth, maxDepth, files) {
|
|
47
|
+
if (maxDepth > 0 && depth >= maxDepth)
|
|
48
|
+
return;
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (["node_modules", ".git", "dist", "__pycache__"].includes(entry.name))
|
|
60
|
+
continue;
|
|
61
|
+
collectFiles(fullPath, depth + 1, maxDepth, files);
|
|
62
|
+
}
|
|
63
|
+
else if (entry.isFile()) {
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.statSync(fullPath);
|
|
66
|
+
files.push({
|
|
67
|
+
path: fullPath,
|
|
68
|
+
size: stat.size,
|
|
69
|
+
modified: stat.mtime.toISOString(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Skip inaccessible files
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function fileStats(options) {
|
|
79
|
+
const { dirPath, maxDepth = 10 } = options;
|
|
80
|
+
const resolvedPath = path.resolve(dirPath);
|
|
81
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
82
|
+
throw new Error(`Directory not found: ${resolvedPath}`);
|
|
83
|
+
}
|
|
84
|
+
const files = [];
|
|
85
|
+
collectFiles(resolvedPath, 0, maxDepth, files);
|
|
86
|
+
const totalSizeBytes = files.reduce((sum, f) => sum + f.size, 0);
|
|
87
|
+
// Count by extension
|
|
88
|
+
const extMap = {};
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const ext = path.extname(file.path).toLowerCase() || "(no extension)";
|
|
91
|
+
if (!extMap[ext]) {
|
|
92
|
+
extMap[ext] = { count: 0, totalSizeBytes: 0 };
|
|
93
|
+
}
|
|
94
|
+
extMap[ext].count++;
|
|
95
|
+
extMap[ext].totalSizeBytes += file.size;
|
|
96
|
+
}
|
|
97
|
+
const extensionCounts = {};
|
|
98
|
+
const sortedExts = Object.entries(extMap).sort((a, b) => b[1].count - a[1].count);
|
|
99
|
+
for (const [ext, data] of sortedExts) {
|
|
100
|
+
extensionCounts[ext] = {
|
|
101
|
+
count: data.count,
|
|
102
|
+
totalSize: formatSize(data.totalSizeBytes),
|
|
103
|
+
totalSizeBytes: data.totalSizeBytes,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Largest files (top 10)
|
|
107
|
+
const largestFiles = [...files]
|
|
108
|
+
.sort((a, b) => b.size - a.size)
|
|
109
|
+
.slice(0, 10);
|
|
110
|
+
// Newest files (top 10)
|
|
111
|
+
const newestFiles = [...files]
|
|
112
|
+
.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime())
|
|
113
|
+
.slice(0, 10);
|
|
114
|
+
// Oldest files (top 10)
|
|
115
|
+
const oldestFiles = [...files]
|
|
116
|
+
.sort((a, b) => new Date(a.modified).getTime() - new Date(b.modified).getTime())
|
|
117
|
+
.slice(0, 10);
|
|
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/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rog0x/mcp-file-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server providing file and directory analysis tools for AI agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-file-tools": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "ts-node src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"claude",
|
|
17
|
+
"ai",
|
|
18
|
+
"file-tools",
|
|
19
|
+
"directory-tree",
|
|
20
|
+
"code-counter",
|
|
21
|
+
"duplicate-finder"
|
|
22
|
+
],
|
|
23
|
+
"author": "rog0x",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/rog0x/mcp-file-tools"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
31
|
+
"glob": "^11.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"typescript": "^5.4.0"
|
|
36
|
+
}
|
|
37
|
+
}
|