bach-filecommander-mcp 1.3.0 → 1.4.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/CHANGELOG.md +14 -0
- package/README.md +31 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1036 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* See LICENSE file for details.
|
|
10
10
|
*
|
|
11
11
|
* @author Lukas (BACH)
|
|
12
|
-
* @version 1.
|
|
12
|
+
* @version 1.4.0
|
|
13
13
|
* @license MIT
|
|
14
14
|
*/
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -18,6 +18,8 @@ import { z } from "zod";
|
|
|
18
18
|
import * as fs from "fs/promises";
|
|
19
19
|
import * as fsSync from "fs";
|
|
20
20
|
import * as path from "path";
|
|
21
|
+
import * as crypto from "crypto";
|
|
22
|
+
import * as os from "os";
|
|
21
23
|
import { exec, spawn } from "child_process";
|
|
22
24
|
import { promisify } from "util";
|
|
23
25
|
const execAsync = promisify(exec);
|
|
@@ -26,7 +28,7 @@ const execAsync = promisify(exec);
|
|
|
26
28
|
// ============================================================================
|
|
27
29
|
const server = new McpServer({
|
|
28
30
|
name: "bach-filecommander-mcp",
|
|
29
|
-
version: "1.
|
|
31
|
+
version: "1.4.0"
|
|
30
32
|
});
|
|
31
33
|
const processSessions = new Map();
|
|
32
34
|
let sessionCounter = 0;
|
|
@@ -2023,6 +2025,1038 @@ Args:
|
|
|
2023
2025
|
}
|
|
2024
2026
|
});
|
|
2025
2027
|
// ============================================================================
|
|
2028
|
+
// Tool: Fix JSON
|
|
2029
|
+
// ============================================================================
|
|
2030
|
+
server.registerTool("fc_fix_json", {
|
|
2031
|
+
title: "JSON reparieren",
|
|
2032
|
+
description: `Repariert häufige JSON-Fehler automatisch.
|
|
2033
|
+
|
|
2034
|
+
Args:
|
|
2035
|
+
- path (string): Pfad zur JSON-Datei
|
|
2036
|
+
- dry_run (boolean, optional): Nur Probleme anzeigen, nicht reparieren
|
|
2037
|
+
- create_backup (boolean, optional): Backup erstellen vor Reparatur
|
|
2038
|
+
|
|
2039
|
+
Repariert: BOM, Trailing Commas, Single Quotes, Kommentare, NUL-Bytes`,
|
|
2040
|
+
inputSchema: {
|
|
2041
|
+
path: z.string().min(1).describe("Pfad zur JSON-Datei"),
|
|
2042
|
+
dry_run: z.boolean().default(false).describe("Nur Probleme anzeigen"),
|
|
2043
|
+
create_backup: z.boolean().default(true).describe("Backup erstellen")
|
|
2044
|
+
},
|
|
2045
|
+
annotations: {
|
|
2046
|
+
readOnlyHint: false,
|
|
2047
|
+
destructiveHint: false,
|
|
2048
|
+
idempotentHint: true,
|
|
2049
|
+
openWorldHint: false
|
|
2050
|
+
}
|
|
2051
|
+
}, async (params) => {
|
|
2052
|
+
try {
|
|
2053
|
+
const filePath = normalizePath(params.path);
|
|
2054
|
+
if (!await pathExists(filePath)) {
|
|
2055
|
+
return { isError: true, content: [{ type: "text", text: `❌ Datei nicht gefunden: ${filePath}` }] };
|
|
2056
|
+
}
|
|
2057
|
+
const rawContent = await fs.readFile(filePath, "utf-8");
|
|
2058
|
+
const fixes = [];
|
|
2059
|
+
let content = rawContent;
|
|
2060
|
+
// Remove BOM
|
|
2061
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
2062
|
+
content = content.slice(1);
|
|
2063
|
+
fixes.push("UTF-8 BOM entfernt");
|
|
2064
|
+
}
|
|
2065
|
+
// Remove NUL bytes
|
|
2066
|
+
if (content.includes('\0')) {
|
|
2067
|
+
content = content.replace(/\0/g, '');
|
|
2068
|
+
fixes.push("NUL-Bytes entfernt");
|
|
2069
|
+
}
|
|
2070
|
+
// Remove single-line comments
|
|
2071
|
+
const c1 = content;
|
|
2072
|
+
content = content.replace(/^(\s*)\/\/.*$/gm, '');
|
|
2073
|
+
if (content !== c1)
|
|
2074
|
+
fixes.push("Einzeilige Kommentare entfernt");
|
|
2075
|
+
// Remove multi-line comments
|
|
2076
|
+
const c2 = content;
|
|
2077
|
+
content = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
2078
|
+
if (content !== c2)
|
|
2079
|
+
fixes.push("Mehrzeilige Kommentare entfernt");
|
|
2080
|
+
// Fix trailing commas before } or ]
|
|
2081
|
+
const c3 = content;
|
|
2082
|
+
content = content.replace(/,(\s*[}\]])/g, '$1');
|
|
2083
|
+
if (content !== c3)
|
|
2084
|
+
fixes.push("Trailing Commas entfernt");
|
|
2085
|
+
// Fix single quotes to double quotes for keys and simple values
|
|
2086
|
+
const c4 = content;
|
|
2087
|
+
content = content.replace(/(\s*)'([^'\\]*(?:\\.[^'\\]*)*)'\s*:/g, '$1"$2":');
|
|
2088
|
+
content = content.replace(/:\s*'([^'\\]*(?:\\.[^'\\]*)*)'/g, ': "$1"');
|
|
2089
|
+
if (content !== c4)
|
|
2090
|
+
fixes.push("Single Quotes → Double Quotes");
|
|
2091
|
+
// Try to parse
|
|
2092
|
+
let isValid = false;
|
|
2093
|
+
let parseError = '';
|
|
2094
|
+
try {
|
|
2095
|
+
JSON.parse(content);
|
|
2096
|
+
isValid = true;
|
|
2097
|
+
}
|
|
2098
|
+
catch (e) {
|
|
2099
|
+
parseError = e instanceof Error ? e.message : String(e);
|
|
2100
|
+
}
|
|
2101
|
+
if (fixes.length === 0 && isValid) {
|
|
2102
|
+
return { content: [{ type: "text", text: `✅ ${path.basename(filePath)} ist bereits gültiges JSON.` }] };
|
|
2103
|
+
}
|
|
2104
|
+
if (params.dry_run) {
|
|
2105
|
+
return {
|
|
2106
|
+
content: [{ type: "text", text: [
|
|
2107
|
+
`🔍 **JSON-Analyse: ${path.basename(filePath)}**`, '',
|
|
2108
|
+
fixes.length > 0 ? `**Gefundene Probleme:**` : `Keine automatisch reparierbaren Probleme.`,
|
|
2109
|
+
...fixes.map(f => ` - ${f}`), '',
|
|
2110
|
+
isValid ? `✅ Nach Reparatur: Gültiges JSON` : `⚠️ Nach Reparatur noch ungültig: ${parseError}`
|
|
2111
|
+
].join('\n') }]
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
if (params.create_backup && fixes.length > 0) {
|
|
2115
|
+
await fs.writeFile(filePath + '.bak', rawContent, "utf-8");
|
|
2116
|
+
}
|
|
2117
|
+
if (isValid) {
|
|
2118
|
+
content = JSON.stringify(JSON.parse(content), null, 2);
|
|
2119
|
+
}
|
|
2120
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
2121
|
+
return {
|
|
2122
|
+
content: [{ type: "text", text: [
|
|
2123
|
+
`✅ **JSON repariert: ${path.basename(filePath)}**`, '',
|
|
2124
|
+
...fixes.map(f => ` - ${f}`), '',
|
|
2125
|
+
isValid ? `✅ Gültiges JSON` : `⚠️ Noch ungültig: ${parseError}`,
|
|
2126
|
+
params.create_backup ? `📋 Backup: ${filePath}.bak` : ''
|
|
2127
|
+
].join('\n') }]
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
catch (error) {
|
|
2131
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
// ============================================================================
|
|
2135
|
+
// Tool: Validate JSON
|
|
2136
|
+
// ============================================================================
|
|
2137
|
+
server.registerTool("fc_validate_json", {
|
|
2138
|
+
title: "JSON validieren",
|
|
2139
|
+
description: `Validiert eine JSON-Datei und zeigt detaillierte Fehlerinformationen.
|
|
2140
|
+
|
|
2141
|
+
Args:
|
|
2142
|
+
- path (string): Pfad zur JSON-Datei
|
|
2143
|
+
|
|
2144
|
+
Returns:
|
|
2145
|
+
- Validierungsstatus mit Zeile/Spalte bei Fehlern`,
|
|
2146
|
+
inputSchema: {
|
|
2147
|
+
path: z.string().min(1).describe("Pfad zur JSON-Datei")
|
|
2148
|
+
},
|
|
2149
|
+
annotations: {
|
|
2150
|
+
readOnlyHint: true,
|
|
2151
|
+
destructiveHint: false,
|
|
2152
|
+
idempotentHint: true,
|
|
2153
|
+
openWorldHint: false
|
|
2154
|
+
}
|
|
2155
|
+
}, async (params) => {
|
|
2156
|
+
try {
|
|
2157
|
+
const filePath = normalizePath(params.path);
|
|
2158
|
+
if (!await pathExists(filePath)) {
|
|
2159
|
+
return { isError: true, content: [{ type: "text", text: `❌ Datei nicht gefunden: ${filePath}` }] };
|
|
2160
|
+
}
|
|
2161
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
2162
|
+
const stats = await fs.stat(filePath);
|
|
2163
|
+
try {
|
|
2164
|
+
const parsed = JSON.parse(content);
|
|
2165
|
+
const keyCount = typeof parsed === 'object' && parsed !== null ? Object.keys(parsed).length : 0;
|
|
2166
|
+
const type = Array.isArray(parsed) ? `Array (${parsed.length} Elemente)` : typeof parsed === 'object' && parsed !== null ? `Objekt (${keyCount} Schlüssel)` : typeof parsed;
|
|
2167
|
+
return {
|
|
2168
|
+
content: [{ type: "text", text: [
|
|
2169
|
+
`✅ **Gültiges JSON: ${path.basename(filePath)}**`, '',
|
|
2170
|
+
`| Eigenschaft | Wert |`, `|---|---|`,
|
|
2171
|
+
`| Typ | ${type} |`,
|
|
2172
|
+
`| Größe | ${formatFileSize(stats.size)} |`,
|
|
2173
|
+
`| BOM | ${content.charCodeAt(0) === 0xFEFF ? '⚠️ Ja' : 'Nein'} |`,
|
|
2174
|
+
`| Encoding | UTF-8 |`
|
|
2175
|
+
].join('\n') }]
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
catch (e) {
|
|
2179
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
2180
|
+
// Extract position from error message
|
|
2181
|
+
const posMatch = errorMsg.match(/position\s+(\d+)/i);
|
|
2182
|
+
let lineInfo = '';
|
|
2183
|
+
if (posMatch) {
|
|
2184
|
+
const pos = parseInt(posMatch[1]);
|
|
2185
|
+
const before = content.substring(0, pos);
|
|
2186
|
+
const line = before.split('\n').length;
|
|
2187
|
+
const col = pos - before.lastIndexOf('\n');
|
|
2188
|
+
const lines = content.split('\n');
|
|
2189
|
+
const contextLines = lines.slice(Math.max(0, line - 3), line + 2);
|
|
2190
|
+
lineInfo = `\n**Fehlerposition:** Zeile ${line}, Spalte ${col}\n\n\`\`\`\n${contextLines.map((l, i) => `${Math.max(1, line - 2) + i}: ${l}`).join('\n')}\n\`\`\``;
|
|
2191
|
+
}
|
|
2192
|
+
return {
|
|
2193
|
+
content: [{ type: "text", text: `❌ **Ungültiges JSON: ${path.basename(filePath)}**\n\n**Fehler:** ${errorMsg}${lineInfo}\n\n💡 Nutze \`fc_fix_json\` für automatische Reparatur.` }]
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
catch (error) {
|
|
2198
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
// ============================================================================
|
|
2202
|
+
// Tool: Cleanup File
|
|
2203
|
+
// ============================================================================
|
|
2204
|
+
server.registerTool("fc_cleanup_file", {
|
|
2205
|
+
title: "Datei bereinigen",
|
|
2206
|
+
description: `Bereinigt eine oder mehrere Dateien von häufigen Problemen.
|
|
2207
|
+
|
|
2208
|
+
Args:
|
|
2209
|
+
- path (string): Pfad zu Datei oder Verzeichnis
|
|
2210
|
+
- recursive (boolean, optional): Bei Verzeichnis rekursiv
|
|
2211
|
+
- extensions (string, optional): Dateierweiterungen filtern (z.B. ".txt,.json,.py")
|
|
2212
|
+
- remove_bom (boolean): UTF-8 BOM entfernen
|
|
2213
|
+
- remove_trailing_whitespace (boolean): Trailing Whitespace entfernen
|
|
2214
|
+
- normalize_line_endings (string, optional): "lf" | "crlf" | null
|
|
2215
|
+
- remove_nul_bytes (boolean): NUL-Bytes entfernen
|
|
2216
|
+
- dry_run (boolean): Nur anzeigen
|
|
2217
|
+
|
|
2218
|
+
Bereinigt: BOM, NUL-Bytes, Trailing Whitespace, Line Endings`,
|
|
2219
|
+
inputSchema: {
|
|
2220
|
+
path: z.string().min(1).describe("Pfad zu Datei/Verzeichnis"),
|
|
2221
|
+
recursive: z.boolean().default(false).describe("Rekursiv"),
|
|
2222
|
+
extensions: z.string().optional().describe("Erweiterungen filtern (.txt,.json)"),
|
|
2223
|
+
remove_bom: z.boolean().default(true).describe("BOM entfernen"),
|
|
2224
|
+
remove_trailing_whitespace: z.boolean().default(true).describe("Trailing Whitespace"),
|
|
2225
|
+
normalize_line_endings: z.enum(["lf", "crlf"]).optional().describe("Line Endings"),
|
|
2226
|
+
remove_nul_bytes: z.boolean().default(true).describe("NUL-Bytes entfernen"),
|
|
2227
|
+
dry_run: z.boolean().default(false).describe("Nur anzeigen")
|
|
2228
|
+
},
|
|
2229
|
+
annotations: {
|
|
2230
|
+
readOnlyHint: false,
|
|
2231
|
+
destructiveHint: false,
|
|
2232
|
+
idempotentHint: true,
|
|
2233
|
+
openWorldHint: false
|
|
2234
|
+
}
|
|
2235
|
+
}, async (params) => {
|
|
2236
|
+
try {
|
|
2237
|
+
const targetPath = normalizePath(params.path);
|
|
2238
|
+
if (!await pathExists(targetPath)) {
|
|
2239
|
+
return { isError: true, content: [{ type: "text", text: `❌ Pfad nicht gefunden: ${targetPath}` }] };
|
|
2240
|
+
}
|
|
2241
|
+
const stats = await fs.stat(targetPath);
|
|
2242
|
+
const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
|
|
2243
|
+
// Collect files
|
|
2244
|
+
const files = [];
|
|
2245
|
+
if (stats.isDirectory()) {
|
|
2246
|
+
async function collectFiles(dir) {
|
|
2247
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2248
|
+
for (const entry of entries) {
|
|
2249
|
+
const full = path.join(dir, entry.name);
|
|
2250
|
+
if (entry.isDirectory() && params.recursive) {
|
|
2251
|
+
if (!['node_modules', '.git', '$RECYCLE.BIN'].includes(entry.name)) {
|
|
2252
|
+
await collectFiles(full);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
else if (entry.isFile()) {
|
|
2256
|
+
if (!extFilter || extFilter.includes(path.extname(entry.name).toLowerCase())) {
|
|
2257
|
+
files.push(full);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
await collectFiles(targetPath);
|
|
2263
|
+
}
|
|
2264
|
+
else {
|
|
2265
|
+
files.push(targetPath);
|
|
2266
|
+
}
|
|
2267
|
+
const results = [];
|
|
2268
|
+
let totalFixed = 0;
|
|
2269
|
+
for (const filePath of files) {
|
|
2270
|
+
try {
|
|
2271
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
2272
|
+
let content = raw;
|
|
2273
|
+
const fixes = [];
|
|
2274
|
+
if (params.remove_bom && content.charCodeAt(0) === 0xFEFF) {
|
|
2275
|
+
content = content.slice(1);
|
|
2276
|
+
fixes.push("BOM");
|
|
2277
|
+
}
|
|
2278
|
+
if (params.remove_nul_bytes && content.includes('\0')) {
|
|
2279
|
+
content = content.replace(/\0/g, '');
|
|
2280
|
+
fixes.push("NUL");
|
|
2281
|
+
}
|
|
2282
|
+
if (params.remove_trailing_whitespace) {
|
|
2283
|
+
const c = content;
|
|
2284
|
+
content = content.replace(/[ \t]+$/gm, '');
|
|
2285
|
+
if (content !== c)
|
|
2286
|
+
fixes.push("Whitespace");
|
|
2287
|
+
}
|
|
2288
|
+
if (params.normalize_line_endings) {
|
|
2289
|
+
const c = content;
|
|
2290
|
+
content = content.replace(/\r\n/g, '\n');
|
|
2291
|
+
if (params.normalize_line_endings === 'crlf') {
|
|
2292
|
+
content = content.replace(/\n/g, '\r\n');
|
|
2293
|
+
}
|
|
2294
|
+
if (content !== c)
|
|
2295
|
+
fixes.push(params.normalize_line_endings.toUpperCase());
|
|
2296
|
+
}
|
|
2297
|
+
if (fixes.length > 0) {
|
|
2298
|
+
if (!params.dry_run) {
|
|
2299
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
2300
|
+
}
|
|
2301
|
+
results.push(` ✅ ${path.relative(targetPath, filePath) || path.basename(filePath)} [${fixes.join(', ')}]`);
|
|
2302
|
+
totalFixed++;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
catch {
|
|
2306
|
+
// Skip binary/unreadable files
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (totalFixed === 0) {
|
|
2310
|
+
return { content: [{ type: "text", text: `✅ Keine Bereinigung nötig. ${files.length} Dateien geprüft.` }] };
|
|
2311
|
+
}
|
|
2312
|
+
return {
|
|
2313
|
+
content: [{ type: "text", text: [
|
|
2314
|
+
`${params.dry_run ? '🔍 **Vorschau**' : '✅ **Bereinigt**'}: ${totalFixed}/${files.length} Dateien`, '',
|
|
2315
|
+
...results
|
|
2316
|
+
].join('\n') }]
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
catch (error) {
|
|
2320
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
// ============================================================================
|
|
2324
|
+
// Tool: Fix Encoding
|
|
2325
|
+
// ============================================================================
|
|
2326
|
+
server.registerTool("fc_fix_encoding", {
|
|
2327
|
+
title: "Encoding reparieren",
|
|
2328
|
+
description: `Erkennt und repariert Encoding-Fehler (Mojibake, doppeltes UTF-8).
|
|
2329
|
+
|
|
2330
|
+
Args:
|
|
2331
|
+
- path (string): Pfad zur Datei
|
|
2332
|
+
- dry_run (boolean): Nur Probleme anzeigen
|
|
2333
|
+
- create_backup (boolean): Backup erstellen
|
|
2334
|
+
|
|
2335
|
+
Repariert häufige Mojibake-Muster wie:
|
|
2336
|
+
- ä → ä, ö → ö, ü → ü
|
|
2337
|
+
- Ä → Ä, Ö → Ö, Ü → Ü
|
|
2338
|
+
- ß → ß, € → €`,
|
|
2339
|
+
inputSchema: {
|
|
2340
|
+
path: z.string().min(1).describe("Pfad zur Datei"),
|
|
2341
|
+
dry_run: z.boolean().default(false).describe("Nur anzeigen"),
|
|
2342
|
+
create_backup: z.boolean().default(true).describe("Backup erstellen")
|
|
2343
|
+
},
|
|
2344
|
+
annotations: {
|
|
2345
|
+
readOnlyHint: false,
|
|
2346
|
+
destructiveHint: false,
|
|
2347
|
+
idempotentHint: true,
|
|
2348
|
+
openWorldHint: false
|
|
2349
|
+
}
|
|
2350
|
+
}, async (params) => {
|
|
2351
|
+
try {
|
|
2352
|
+
const filePath = normalizePath(params.path);
|
|
2353
|
+
if (!await pathExists(filePath)) {
|
|
2354
|
+
return { isError: true, content: [{ type: "text", text: `❌ Datei nicht gefunden: ${filePath}` }] };
|
|
2355
|
+
}
|
|
2356
|
+
const rawContent = await fs.readFile(filePath, "utf-8");
|
|
2357
|
+
// Common Mojibake patterns (UTF-8 decoded as Latin-1 then re-encoded as UTF-8)
|
|
2358
|
+
const mojibakeMap = [
|
|
2359
|
+
[/ä/g, 'ä', 'ä'], [/ö/g, 'ö', 'ö'], [/ü/g, 'ü', 'ü'],
|
|
2360
|
+
[/Ä/g, 'Ä', 'Ä'], [/Ö/g, 'Ö', 'Ö'], [/Ü/g, 'Ü', 'Ü'],
|
|
2361
|
+
[/ß/g, 'ß', 'ß'], [/€/g, '€', '€'],
|
|
2362
|
+
[/é/g, 'é', 'é'], [/è/g, 'è', 'è'],
|
|
2363
|
+
[/à /g, 'à', 'à'], [/á/g, 'á', 'á'],
|
|
2364
|
+
[/î/g, 'î', 'î'], [/ï/g, 'ï', 'ï'],
|
|
2365
|
+
[/ô/g, 'ô', 'ô'], [/ù/g, 'ù', 'ù'],
|
|
2366
|
+
[/ç/g, 'ç', 'ç'], [/ñ/g, 'ñ', 'ñ'],
|
|
2367
|
+
[/\u00e2\u0080\u0093/g, '\u2013', 'en-dash'], [/\u00e2\u0080\u0094/g, '\u2014', 'em-dash'],
|
|
2368
|
+
[/\u00e2\u0080\u009c/g, '\u201C', 'left-dquote'], [/\u00e2\u0080\u009d/g, '\u201D', 'right-dquote'],
|
|
2369
|
+
[/\u00e2\u0080\u0098/g, '\u2018', 'left-squote'], [/\u00e2\u0080\u0099/g, '\u2019', 'right-squote'],
|
|
2370
|
+
[/\u00c2\u00a0/g, ' ', 'NBSP'], [/\u00c2\u00a9/g, '\u00A9', '\u00A9'],
|
|
2371
|
+
[/\u00c2\u00ae/g, '\u00AE', '\u00AE'], [/\u00c2\u00b0/g, '\u00B0', '\u00B0'],
|
|
2372
|
+
];
|
|
2373
|
+
let content = rawContent;
|
|
2374
|
+
const fixes = [];
|
|
2375
|
+
for (const [pattern, replacement, label] of mojibakeMap) {
|
|
2376
|
+
const before = content;
|
|
2377
|
+
content = content.replace(pattern, replacement);
|
|
2378
|
+
if (content !== before) {
|
|
2379
|
+
const count = (before.match(pattern) || []).length;
|
|
2380
|
+
fixes.push(`${label} (${count}x)`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (fixes.length === 0) {
|
|
2384
|
+
return { content: [{ type: "text", text: `✅ Keine Encoding-Fehler in ${path.basename(filePath)} gefunden.` }] };
|
|
2385
|
+
}
|
|
2386
|
+
if (params.dry_run) {
|
|
2387
|
+
return {
|
|
2388
|
+
content: [{ type: "text", text: [
|
|
2389
|
+
`🔍 **Encoding-Analyse: ${path.basename(filePath)}**`, '',
|
|
2390
|
+
`**Gefundene Mojibake-Muster:**`,
|
|
2391
|
+
...fixes.map(f => ` - ${f}`)
|
|
2392
|
+
].join('\n') }]
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
if (params.create_backup) {
|
|
2396
|
+
await fs.writeFile(filePath + '.bak', rawContent, "utf-8");
|
|
2397
|
+
}
|
|
2398
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
2399
|
+
return {
|
|
2400
|
+
content: [{ type: "text", text: [
|
|
2401
|
+
`✅ **Encoding repariert: ${path.basename(filePath)}**`, '',
|
|
2402
|
+
...fixes.map(f => ` - ${f}`),
|
|
2403
|
+
params.create_backup ? `\n📋 Backup: ${filePath}.bak` : ''
|
|
2404
|
+
].join('\n') }]
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
catch (error) {
|
|
2408
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
// ============================================================================
|
|
2412
|
+
// Tool: Folder Diff
|
|
2413
|
+
// ============================================================================
|
|
2414
|
+
server.registerTool("fc_folder_diff", {
|
|
2415
|
+
title: "Verzeichnis-Änderungen erkennen",
|
|
2416
|
+
description: `Vergleicht den aktuellen Zustand eines Verzeichnisses mit einem gespeicherten Snapshot.
|
|
2417
|
+
|
|
2418
|
+
Args:
|
|
2419
|
+
- path (string): Pfad zum Verzeichnis
|
|
2420
|
+
- save_snapshot (boolean): Aktuellen Zustand als neuen Snapshot speichern
|
|
2421
|
+
- extensions (string, optional): Dateierweiterungen filtern
|
|
2422
|
+
|
|
2423
|
+
Erkennt: Neue Dateien, geänderte Dateien, gelöschte Dateien
|
|
2424
|
+
Snapshots werden in %TEMP%/.fc_snapshots/ gespeichert.`,
|
|
2425
|
+
inputSchema: {
|
|
2426
|
+
path: z.string().min(1).describe("Pfad zum Verzeichnis"),
|
|
2427
|
+
save_snapshot: z.boolean().default(true).describe("Snapshot speichern"),
|
|
2428
|
+
extensions: z.string().optional().describe("Erweiterungen filtern")
|
|
2429
|
+
},
|
|
2430
|
+
annotations: {
|
|
2431
|
+
readOnlyHint: true,
|
|
2432
|
+
destructiveHint: false,
|
|
2433
|
+
idempotentHint: true,
|
|
2434
|
+
openWorldHint: false
|
|
2435
|
+
}
|
|
2436
|
+
}, async (params) => {
|
|
2437
|
+
try {
|
|
2438
|
+
const dirPath = normalizePath(params.path);
|
|
2439
|
+
if (!await pathExists(dirPath)) {
|
|
2440
|
+
return { isError: true, content: [{ type: "text", text: `❌ Verzeichnis nicht gefunden: ${dirPath}` }] };
|
|
2441
|
+
}
|
|
2442
|
+
const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
|
|
2443
|
+
const snapshotDir = path.join(os.tmpdir(), '.fc_snapshots');
|
|
2444
|
+
const snapshotId = crypto.createHash('md5').update(dirPath).digest('hex');
|
|
2445
|
+
const snapshotFile = path.join(snapshotDir, `${snapshotId}.json`);
|
|
2446
|
+
const currentState = {};
|
|
2447
|
+
async function scanDir(dir) {
|
|
2448
|
+
try {
|
|
2449
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2450
|
+
for (const entry of entries) {
|
|
2451
|
+
const full = path.join(dir, entry.name);
|
|
2452
|
+
if (entry.isDirectory()) {
|
|
2453
|
+
if (!['node_modules', '.git', '$RECYCLE.BIN'].includes(entry.name)) {
|
|
2454
|
+
await scanDir(full);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
else if (entry.isFile()) {
|
|
2458
|
+
if (!extFilter || extFilter.includes(path.extname(entry.name).toLowerCase())) {
|
|
2459
|
+
const stats = await fs.stat(full);
|
|
2460
|
+
const rel = path.relative(dirPath, full);
|
|
2461
|
+
currentState[rel] = { size: stats.size, mtime: stats.mtimeMs };
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
catch { /* skip inaccessible dirs */ }
|
|
2467
|
+
}
|
|
2468
|
+
await scanDir(dirPath);
|
|
2469
|
+
// Load previous snapshot
|
|
2470
|
+
let previousState = {};
|
|
2471
|
+
let hasSnapshot = false;
|
|
2472
|
+
try {
|
|
2473
|
+
const data = await fs.readFile(snapshotFile, "utf-8");
|
|
2474
|
+
previousState = JSON.parse(data);
|
|
2475
|
+
hasSnapshot = true;
|
|
2476
|
+
}
|
|
2477
|
+
catch { /* no previous snapshot */ }
|
|
2478
|
+
// Compare
|
|
2479
|
+
const newFiles = [];
|
|
2480
|
+
const modifiedFiles = [];
|
|
2481
|
+
const deletedFiles = [];
|
|
2482
|
+
for (const [rel, entry] of Object.entries(currentState)) {
|
|
2483
|
+
if (!previousState[rel]) {
|
|
2484
|
+
newFiles.push(rel);
|
|
2485
|
+
}
|
|
2486
|
+
else if (entry.size !== previousState[rel].size || Math.abs(entry.mtime - previousState[rel].mtime) > 1000) {
|
|
2487
|
+
modifiedFiles.push(rel);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
for (const rel of Object.keys(previousState)) {
|
|
2491
|
+
if (!currentState[rel]) {
|
|
2492
|
+
deletedFiles.push(rel);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
// Save snapshot
|
|
2496
|
+
if (params.save_snapshot) {
|
|
2497
|
+
await fs.mkdir(snapshotDir, { recursive: true });
|
|
2498
|
+
await fs.writeFile(snapshotFile, JSON.stringify(currentState), "utf-8");
|
|
2499
|
+
}
|
|
2500
|
+
const totalFiles = Object.keys(currentState).length;
|
|
2501
|
+
const totalChanges = newFiles.length + modifiedFiles.length + deletedFiles.length;
|
|
2502
|
+
if (!hasSnapshot) {
|
|
2503
|
+
return {
|
|
2504
|
+
content: [{ type: "text", text: [
|
|
2505
|
+
`📸 **Erster Snapshot erstellt: ${path.basename(dirPath)}**`, '',
|
|
2506
|
+
`| | |`, `|---|---|`,
|
|
2507
|
+
`| Dateien | ${totalFiles} |`,
|
|
2508
|
+
`| Snapshot | ${snapshotFile} |`, '',
|
|
2509
|
+
`Beim nächsten Aufruf werden Änderungen erkannt.`
|
|
2510
|
+
].join('\n') }]
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
if (totalChanges === 0) {
|
|
2514
|
+
return { content: [{ type: "text", text: `✅ Keine Änderungen in ${path.basename(dirPath)}. ${totalFiles} Dateien geprüft.` }] };
|
|
2515
|
+
}
|
|
2516
|
+
const output = [
|
|
2517
|
+
`📊 **Verzeichnis-Diff: ${path.basename(dirPath)}**`, '',
|
|
2518
|
+
`| Kategorie | Anzahl |`, `|---|---|`,
|
|
2519
|
+
`| Neue Dateien | ${newFiles.length} |`,
|
|
2520
|
+
`| Geändert | ${modifiedFiles.length} |`,
|
|
2521
|
+
`| Gelöscht | ${deletedFiles.length} |`,
|
|
2522
|
+
`| Unverändert | ${totalFiles - newFiles.length - modifiedFiles.length} |`
|
|
2523
|
+
];
|
|
2524
|
+
if (newFiles.length > 0) {
|
|
2525
|
+
output.push('', '**Neue Dateien:**', ...newFiles.slice(0, 50).map(f => ` 🟢 ${f}`));
|
|
2526
|
+
if (newFiles.length > 50)
|
|
2527
|
+
output.push(` ... und ${newFiles.length - 50} weitere`);
|
|
2528
|
+
}
|
|
2529
|
+
if (modifiedFiles.length > 0) {
|
|
2530
|
+
output.push('', '**Geänderte Dateien:**', ...modifiedFiles.slice(0, 50).map(f => ` 🟡 ${f}`));
|
|
2531
|
+
if (modifiedFiles.length > 50)
|
|
2532
|
+
output.push(` ... und ${modifiedFiles.length - 50} weitere`);
|
|
2533
|
+
}
|
|
2534
|
+
if (deletedFiles.length > 0) {
|
|
2535
|
+
output.push('', '**Gelöschte Dateien:**', ...deletedFiles.slice(0, 50).map(f => ` 🔴 ${f}`));
|
|
2536
|
+
if (deletedFiles.length > 50)
|
|
2537
|
+
output.push(` ... und ${deletedFiles.length - 50} weitere`);
|
|
2538
|
+
}
|
|
2539
|
+
return { content: [{ type: "text", text: output.join('\n') }] };
|
|
2540
|
+
}
|
|
2541
|
+
catch (error) {
|
|
2542
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
// ============================================================================
|
|
2546
|
+
// Tool: Batch Rename
|
|
2547
|
+
// ============================================================================
|
|
2548
|
+
server.registerTool("fc_batch_rename", {
|
|
2549
|
+
title: "Batch-Umbenennung",
|
|
2550
|
+
description: `Benennt Dateien nach Muster um: Prefix/Suffix entfernen, ersetzen, oder Pattern.
|
|
2551
|
+
|
|
2552
|
+
Args:
|
|
2553
|
+
- directory (string): Verzeichnis mit den Dateien
|
|
2554
|
+
- mode (string): "remove_prefix" | "remove_suffix" | "replace" | "auto_detect"
|
|
2555
|
+
- pattern (string, optional): Zu entfernender/ersetzender Text
|
|
2556
|
+
- replacement (string, optional): Ersetzungstext (für replace-Modus)
|
|
2557
|
+
- extensions (string, optional): Nur bestimmte Erweiterungen
|
|
2558
|
+
- dry_run (boolean): Nur Vorschau
|
|
2559
|
+
|
|
2560
|
+
Beispiele:
|
|
2561
|
+
- Prefix entfernen: mode="remove_prefix", pattern="backup_"
|
|
2562
|
+
- Auto-Detect: mode="auto_detect" erkennt gemeinsame Prefixe`,
|
|
2563
|
+
inputSchema: {
|
|
2564
|
+
directory: z.string().min(1).describe("Verzeichnis"),
|
|
2565
|
+
mode: z.enum(["remove_prefix", "remove_suffix", "replace", "auto_detect"]).describe("Modus"),
|
|
2566
|
+
pattern: z.string().optional().describe("Zu entfernender/ersetzender Text"),
|
|
2567
|
+
replacement: z.string().default("").describe("Ersetzungstext"),
|
|
2568
|
+
extensions: z.string().optional().describe("Erweiterungen filtern"),
|
|
2569
|
+
dry_run: z.boolean().default(true).describe("Nur Vorschau")
|
|
2570
|
+
},
|
|
2571
|
+
annotations: {
|
|
2572
|
+
readOnlyHint: false,
|
|
2573
|
+
destructiveHint: true,
|
|
2574
|
+
idempotentHint: false,
|
|
2575
|
+
openWorldHint: false
|
|
2576
|
+
}
|
|
2577
|
+
}, async (params) => {
|
|
2578
|
+
try {
|
|
2579
|
+
const dirPath = normalizePath(params.directory);
|
|
2580
|
+
if (!await pathExists(dirPath)) {
|
|
2581
|
+
return { isError: true, content: [{ type: "text", text: `❌ Verzeichnis nicht gefunden: ${dirPath}` }] };
|
|
2582
|
+
}
|
|
2583
|
+
const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
|
|
2584
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
2585
|
+
const files = entries.filter(e => e.isFile() && (!extFilter || extFilter.includes(path.extname(e.name).toLowerCase())));
|
|
2586
|
+
if (files.length === 0) {
|
|
2587
|
+
return { content: [{ type: "text", text: `🔍 Keine passenden Dateien in ${dirPath}` }] };
|
|
2588
|
+
}
|
|
2589
|
+
const renames = [];
|
|
2590
|
+
if (params.mode === 'auto_detect') {
|
|
2591
|
+
// Find common prefix
|
|
2592
|
+
const names = files.map(f => f.name);
|
|
2593
|
+
let commonPrefix = names[0] || '';
|
|
2594
|
+
for (let i = 1; i < names.length; i++) {
|
|
2595
|
+
while (!names[i].startsWith(commonPrefix) && commonPrefix.length > 0) {
|
|
2596
|
+
commonPrefix = commonPrefix.slice(0, -1);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
// Find common suffix (before extension)
|
|
2600
|
+
const stems = files.map(f => path.parse(f.name).name);
|
|
2601
|
+
let commonSuffix = stems[0] || '';
|
|
2602
|
+
for (let i = 1; i < stems.length; i++) {
|
|
2603
|
+
while (!stems[i].endsWith(commonSuffix) && commonSuffix.length > 0) {
|
|
2604
|
+
commonSuffix = commonSuffix.slice(1);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const detections = [];
|
|
2608
|
+
if (commonPrefix.length >= 3)
|
|
2609
|
+
detections.push(`Prefix: "${commonPrefix}"`);
|
|
2610
|
+
if (commonSuffix.length >= 3)
|
|
2611
|
+
detections.push(`Suffix: "${commonSuffix}"`);
|
|
2612
|
+
if (detections.length === 0) {
|
|
2613
|
+
return { content: [{ type: "text", text: `🔍 Kein gemeinsames Muster erkannt bei ${files.length} Dateien.` }] };
|
|
2614
|
+
}
|
|
2615
|
+
// Use prefix if found
|
|
2616
|
+
if (commonPrefix.length >= 3) {
|
|
2617
|
+
for (const f of files) {
|
|
2618
|
+
const newName = f.name.slice(commonPrefix.length);
|
|
2619
|
+
if (newName.length > 0) {
|
|
2620
|
+
renames.push({ old: f.name, new: newName });
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return {
|
|
2625
|
+
content: [{ type: "text", text: [
|
|
2626
|
+
`🔍 **Auto-Detect: ${files.length} Dateien**`, '',
|
|
2627
|
+
`Erkannte Muster: ${detections.join(', ')}`, '',
|
|
2628
|
+
renames.length > 0 ? `**Vorgeschlagene Umbenennung (Prefix "${commonPrefix}" entfernen):**` : '',
|
|
2629
|
+
...renames.slice(0, 30).map(r => ` ${r.old} → ${r.new}`),
|
|
2630
|
+
renames.length > 30 ? ` ... und ${renames.length - 30} weitere` : '', '',
|
|
2631
|
+
`💡 Nutze \`mode="remove_prefix", pattern="${commonPrefix}", dry_run=false\` zum Ausführen.`
|
|
2632
|
+
].join('\n') }]
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
if (!params.pattern) {
|
|
2636
|
+
return { isError: true, content: [{ type: "text", text: `❌ 'pattern' erforderlich für Modus "${params.mode}".` }] };
|
|
2637
|
+
}
|
|
2638
|
+
for (const f of files) {
|
|
2639
|
+
let newName;
|
|
2640
|
+
switch (params.mode) {
|
|
2641
|
+
case 'remove_prefix':
|
|
2642
|
+
newName = f.name.startsWith(params.pattern) ? f.name.slice(params.pattern.length) : f.name;
|
|
2643
|
+
break;
|
|
2644
|
+
case 'remove_suffix': {
|
|
2645
|
+
const parsed = path.parse(f.name);
|
|
2646
|
+
newName = parsed.name.endsWith(params.pattern) ? parsed.name.slice(0, -params.pattern.length) + parsed.ext : f.name;
|
|
2647
|
+
break;
|
|
2648
|
+
}
|
|
2649
|
+
case 'replace':
|
|
2650
|
+
newName = f.name.replace(new RegExp(params.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), params.replacement);
|
|
2651
|
+
break;
|
|
2652
|
+
default:
|
|
2653
|
+
newName = f.name;
|
|
2654
|
+
}
|
|
2655
|
+
if (newName !== f.name && newName.length > 0) {
|
|
2656
|
+
renames.push({ old: f.name, new: newName });
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
if (renames.length === 0) {
|
|
2660
|
+
return { content: [{ type: "text", text: `🔍 Keine Dateien passen zum Muster "${params.pattern}".` }] };
|
|
2661
|
+
}
|
|
2662
|
+
if (params.dry_run) {
|
|
2663
|
+
return {
|
|
2664
|
+
content: [{ type: "text", text: [
|
|
2665
|
+
`🔍 **Vorschau: ${renames.length} Umbenennungen**`, '',
|
|
2666
|
+
...renames.map(r => ` ${r.old} → ${r.new}`), '',
|
|
2667
|
+
`💡 Setze \`dry_run=false\` zum Ausführen.`
|
|
2668
|
+
].join('\n') }]
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
let successCount = 0;
|
|
2672
|
+
const errors = [];
|
|
2673
|
+
for (const r of renames) {
|
|
2674
|
+
try {
|
|
2675
|
+
await fs.rename(path.join(dirPath, r.old), path.join(dirPath, r.new));
|
|
2676
|
+
successCount++;
|
|
2677
|
+
}
|
|
2678
|
+
catch (e) {
|
|
2679
|
+
errors.push(`${r.old}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return {
|
|
2683
|
+
content: [{ type: "text", text: [
|
|
2684
|
+
`✅ **${successCount}/${renames.length} Dateien umbenannt**`,
|
|
2685
|
+
...errors.map(e => ` ❌ ${e}`)
|
|
2686
|
+
].join('\n') }]
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
catch (error) {
|
|
2690
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
// ============================================================================
|
|
2694
|
+
// Tool: Convert Format
|
|
2695
|
+
// ============================================================================
|
|
2696
|
+
server.registerTool("fc_convert_format", {
|
|
2697
|
+
title: "Format konvertieren",
|
|
2698
|
+
description: `Konvertiert Dateien zwischen verschiedenen Formaten.
|
|
2699
|
+
|
|
2700
|
+
Args:
|
|
2701
|
+
- input_path (string): Pfad zur Quelldatei
|
|
2702
|
+
- output_path (string): Pfad zur Zieldatei
|
|
2703
|
+
- input_format (string): "json" | "csv" | "ini"
|
|
2704
|
+
- output_format (string): "json" | "csv" | "ini"
|
|
2705
|
+
- json_indent (number, optional): Einrückung für JSON (default: 2)
|
|
2706
|
+
|
|
2707
|
+
Unterstützte Konvertierungen:
|
|
2708
|
+
- JSON ↔ CSV (bei Arrays von Objekten)
|
|
2709
|
+
- JSON ↔ INI (bei flachen Objekten/Sektionen)
|
|
2710
|
+
- JSON pretty-print / minify`,
|
|
2711
|
+
inputSchema: {
|
|
2712
|
+
input_path: z.string().min(1).describe("Quelldatei"),
|
|
2713
|
+
output_path: z.string().min(1).describe("Zieldatei"),
|
|
2714
|
+
input_format: z.enum(["json", "csv", "ini"]).describe("Eingabeformat"),
|
|
2715
|
+
output_format: z.enum(["json", "csv", "ini"]).describe("Ausgabeformat"),
|
|
2716
|
+
json_indent: z.number().int().min(0).max(8).default(2).describe("JSON Einrückung")
|
|
2717
|
+
},
|
|
2718
|
+
annotations: {
|
|
2719
|
+
readOnlyHint: false,
|
|
2720
|
+
destructiveHint: false,
|
|
2721
|
+
idempotentHint: true,
|
|
2722
|
+
openWorldHint: false
|
|
2723
|
+
}
|
|
2724
|
+
}, async (params) => {
|
|
2725
|
+
try {
|
|
2726
|
+
const inputPath = normalizePath(params.input_path);
|
|
2727
|
+
const outputPath = normalizePath(params.output_path);
|
|
2728
|
+
if (!await pathExists(inputPath)) {
|
|
2729
|
+
return { isError: true, content: [{ type: "text", text: `❌ Quelldatei nicht gefunden: ${inputPath}` }] };
|
|
2730
|
+
}
|
|
2731
|
+
const rawContent = await fs.readFile(inputPath, "utf-8");
|
|
2732
|
+
let data;
|
|
2733
|
+
// Parse input
|
|
2734
|
+
switch (params.input_format) {
|
|
2735
|
+
case 'json':
|
|
2736
|
+
data = JSON.parse(rawContent);
|
|
2737
|
+
break;
|
|
2738
|
+
case 'csv': {
|
|
2739
|
+
const lines = rawContent.trim().split('\n');
|
|
2740
|
+
if (lines.length < 2) {
|
|
2741
|
+
return { isError: true, content: [{ type: "text", text: `❌ CSV benötigt mindestens Header + 1 Datenzeile.` }] };
|
|
2742
|
+
}
|
|
2743
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
2744
|
+
data = lines.slice(1).map(line => {
|
|
2745
|
+
const vals = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
2746
|
+
const obj = {};
|
|
2747
|
+
headers.forEach((h, i) => { obj[h] = vals[i] || ''; });
|
|
2748
|
+
return obj;
|
|
2749
|
+
});
|
|
2750
|
+
break;
|
|
2751
|
+
}
|
|
2752
|
+
case 'ini': {
|
|
2753
|
+
const result = {};
|
|
2754
|
+
let currentSection = '_default';
|
|
2755
|
+
result[currentSection] = {};
|
|
2756
|
+
for (const line of rawContent.split('\n')) {
|
|
2757
|
+
const trimmed = line.trim();
|
|
2758
|
+
if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#'))
|
|
2759
|
+
continue;
|
|
2760
|
+
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
|
|
2761
|
+
if (sectionMatch) {
|
|
2762
|
+
currentSection = sectionMatch[1];
|
|
2763
|
+
result[currentSection] = result[currentSection] || {};
|
|
2764
|
+
}
|
|
2765
|
+
else {
|
|
2766
|
+
const eqIdx = trimmed.indexOf('=');
|
|
2767
|
+
if (eqIdx > 0) {
|
|
2768
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
2769
|
+
const val = trimmed.substring(eqIdx + 1).trim();
|
|
2770
|
+
result[currentSection][key] = val;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
// Remove empty default section
|
|
2775
|
+
if (Object.keys(result._default).length === 0)
|
|
2776
|
+
delete result._default;
|
|
2777
|
+
data = result;
|
|
2778
|
+
break;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
// Generate output
|
|
2782
|
+
let output;
|
|
2783
|
+
switch (params.output_format) {
|
|
2784
|
+
case 'json':
|
|
2785
|
+
output = JSON.stringify(data, null, params.json_indent || undefined);
|
|
2786
|
+
break;
|
|
2787
|
+
case 'csv': {
|
|
2788
|
+
if (!Array.isArray(data)) {
|
|
2789
|
+
return { isError: true, content: [{ type: "text", text: `❌ CSV-Export erfordert ein JSON-Array von Objekten.` }] };
|
|
2790
|
+
}
|
|
2791
|
+
const headers = Object.keys(data[0] || {});
|
|
2792
|
+
const rows = data.map((item) => headers.map(h => {
|
|
2793
|
+
const val = String(item[h] ?? '');
|
|
2794
|
+
return val.includes(',') || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
|
|
2795
|
+
}).join(','));
|
|
2796
|
+
output = [headers.join(','), ...rows].join('\n');
|
|
2797
|
+
break;
|
|
2798
|
+
}
|
|
2799
|
+
case 'ini': {
|
|
2800
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
2801
|
+
return { isError: true, content: [{ type: "text", text: `❌ INI-Export erfordert ein JSON-Objekt.` }] };
|
|
2802
|
+
}
|
|
2803
|
+
const lines = [];
|
|
2804
|
+
for (const [section, values] of Object.entries(data)) {
|
|
2805
|
+
if (typeof values === 'object' && values !== null && !Array.isArray(values)) {
|
|
2806
|
+
lines.push(`[${section}]`);
|
|
2807
|
+
for (const [key, val] of Object.entries(values)) {
|
|
2808
|
+
lines.push(`${key} = ${val}`);
|
|
2809
|
+
}
|
|
2810
|
+
lines.push('');
|
|
2811
|
+
}
|
|
2812
|
+
else {
|
|
2813
|
+
lines.push(`${section} = ${values}`);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
output = lines.join('\n');
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
// Ensure output directory exists
|
|
2821
|
+
const outDir = path.dirname(outputPath);
|
|
2822
|
+
if (!await pathExists(outDir)) {
|
|
2823
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
2824
|
+
}
|
|
2825
|
+
await fs.writeFile(outputPath, output, "utf-8");
|
|
2826
|
+
const outStats = await fs.stat(outputPath);
|
|
2827
|
+
return {
|
|
2828
|
+
content: [{ type: "text", text: [
|
|
2829
|
+
`✅ **Konvertiert: ${params.input_format.toUpperCase()} → ${params.output_format.toUpperCase()}**`, '',
|
|
2830
|
+
`| | |`, `|---|---|`,
|
|
2831
|
+
`| Quelle | ${inputPath} |`,
|
|
2832
|
+
`| Ziel | ${outputPath} |`,
|
|
2833
|
+
`| Größe | ${formatFileSize(outStats.size)} |`
|
|
2834
|
+
].join('\n') }]
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
catch (error) {
|
|
2838
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
// ============================================================================
|
|
2842
|
+
// Tool: Detect Duplicates
|
|
2843
|
+
// ============================================================================
|
|
2844
|
+
server.registerTool("fc_detect_duplicates", {
|
|
2845
|
+
title: "Duplikate erkennen",
|
|
2846
|
+
description: `Findet Datei-Duplikate in einem Verzeichnis anhand von SHA-256 Hashes.
|
|
2847
|
+
|
|
2848
|
+
Args:
|
|
2849
|
+
- directory (string): Verzeichnis zum Scannen
|
|
2850
|
+
- recursive (boolean): Rekursiv suchen
|
|
2851
|
+
- extensions (string, optional): Nur bestimmte Erweiterungen
|
|
2852
|
+
- min_size (number, optional): Mindestgröße in Bytes (default: 1)
|
|
2853
|
+
- max_size (number, optional): Maximale Größe in Bytes
|
|
2854
|
+
|
|
2855
|
+
Returns:
|
|
2856
|
+
- Gruppen von Duplikaten mit Pfaden und Größen`,
|
|
2857
|
+
inputSchema: {
|
|
2858
|
+
directory: z.string().min(1).describe("Verzeichnis"),
|
|
2859
|
+
recursive: z.boolean().default(true).describe("Rekursiv"),
|
|
2860
|
+
extensions: z.string().optional().describe("Erweiterungen filtern"),
|
|
2861
|
+
min_size: z.number().int().min(0).default(1).describe("Mindestgröße in Bytes"),
|
|
2862
|
+
max_size: z.number().int().optional().describe("Maximale Größe in Bytes")
|
|
2863
|
+
},
|
|
2864
|
+
annotations: {
|
|
2865
|
+
readOnlyHint: true,
|
|
2866
|
+
destructiveHint: false,
|
|
2867
|
+
idempotentHint: true,
|
|
2868
|
+
openWorldHint: false
|
|
2869
|
+
}
|
|
2870
|
+
}, async (params) => {
|
|
2871
|
+
try {
|
|
2872
|
+
const dirPath = normalizePath(params.directory);
|
|
2873
|
+
if (!await pathExists(dirPath)) {
|
|
2874
|
+
return { isError: true, content: [{ type: "text", text: `❌ Verzeichnis nicht gefunden: ${dirPath}` }] };
|
|
2875
|
+
}
|
|
2876
|
+
const extFilter = params.extensions ? params.extensions.split(',').map(e => e.trim().toLowerCase()) : null;
|
|
2877
|
+
// Collect files with sizes
|
|
2878
|
+
const files = [];
|
|
2879
|
+
async function collectFiles(dir) {
|
|
2880
|
+
try {
|
|
2881
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2882
|
+
for (const entry of entries) {
|
|
2883
|
+
const full = path.join(dir, entry.name);
|
|
2884
|
+
if (entry.isDirectory() && params.recursive) {
|
|
2885
|
+
if (!['node_modules', '.git', '$RECYCLE.BIN'].includes(entry.name)) {
|
|
2886
|
+
await collectFiles(full);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
else if (entry.isFile()) {
|
|
2890
|
+
if (!extFilter || extFilter.includes(path.extname(entry.name).toLowerCase())) {
|
|
2891
|
+
const stats = await fs.stat(full);
|
|
2892
|
+
if (stats.size >= params.min_size && (!params.max_size || stats.size <= params.max_size)) {
|
|
2893
|
+
files.push({ path: full, size: stats.size });
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
catch { /* skip inaccessible */ }
|
|
2900
|
+
}
|
|
2901
|
+
await collectFiles(dirPath);
|
|
2902
|
+
// Group by size first (quick filter)
|
|
2903
|
+
const sizeGroups = new Map();
|
|
2904
|
+
for (const f of files) {
|
|
2905
|
+
const group = sizeGroups.get(f.size) || [];
|
|
2906
|
+
group.push(f.path);
|
|
2907
|
+
sizeGroups.set(f.size, group);
|
|
2908
|
+
}
|
|
2909
|
+
// Hash only files with matching sizes
|
|
2910
|
+
const hashGroups = new Map();
|
|
2911
|
+
let hashedCount = 0;
|
|
2912
|
+
for (const [size, paths] of sizeGroups) {
|
|
2913
|
+
if (paths.length < 2)
|
|
2914
|
+
continue;
|
|
2915
|
+
for (const filePath of paths) {
|
|
2916
|
+
try {
|
|
2917
|
+
const content = await fs.readFile(filePath);
|
|
2918
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
2919
|
+
hashedCount++;
|
|
2920
|
+
const group = hashGroups.get(hash) || { paths: [], size };
|
|
2921
|
+
group.paths.push(filePath);
|
|
2922
|
+
hashGroups.set(hash, group);
|
|
2923
|
+
}
|
|
2924
|
+
catch { /* skip unreadable */ }
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
// Filter to actual duplicates
|
|
2928
|
+
const duplicates = [...hashGroups.values()].filter(g => g.paths.length > 1);
|
|
2929
|
+
const totalDuplicateFiles = duplicates.reduce((sum, g) => sum + g.paths.length - 1, 0);
|
|
2930
|
+
const totalWastedSpace = duplicates.reduce((sum, g) => sum + g.size * (g.paths.length - 1), 0);
|
|
2931
|
+
if (duplicates.length === 0) {
|
|
2932
|
+
return {
|
|
2933
|
+
content: [{ type: "text", text: `✅ Keine Duplikate gefunden. ${files.length} Dateien geprüft, ${hashedCount} gehasht.` }]
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
const output = [
|
|
2937
|
+
`🔍 **Duplikate gefunden**`, '',
|
|
2938
|
+
`| | |`, `|---|---|`,
|
|
2939
|
+
`| Geprüfte Dateien | ${files.length} |`,
|
|
2940
|
+
`| Duplikat-Gruppen | ${duplicates.length} |`,
|
|
2941
|
+
`| Duplikate gesamt | ${totalDuplicateFiles} |`,
|
|
2942
|
+
`| Verschwendeter Platz | ${formatFileSize(totalWastedSpace)} |`
|
|
2943
|
+
];
|
|
2944
|
+
for (let i = 0; i < Math.min(duplicates.length, 20); i++) {
|
|
2945
|
+
const group = duplicates[i];
|
|
2946
|
+
output.push('', `**Gruppe ${i + 1}** (${formatFileSize(group.size)}):`);
|
|
2947
|
+
for (const p of group.paths) {
|
|
2948
|
+
output.push(` 📄 ${path.relative(dirPath, p)}`);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
if (duplicates.length > 20) {
|
|
2952
|
+
output.push('', `... und ${duplicates.length - 20} weitere Gruppen`);
|
|
2953
|
+
}
|
|
2954
|
+
output.push('', `💡 Nutze \`fc_safe_delete\` zum sicheren Entfernen von Duplikaten.`);
|
|
2955
|
+
return { content: [{ type: "text", text: output.join('\n') }] };
|
|
2956
|
+
}
|
|
2957
|
+
catch (error) {
|
|
2958
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
// ============================================================================
|
|
2962
|
+
// Tool: Markdown to HTML
|
|
2963
|
+
// ============================================================================
|
|
2964
|
+
server.registerTool("fc_md_to_html", {
|
|
2965
|
+
title: "Markdown zu HTML",
|
|
2966
|
+
description: `Konvertiert Markdown zu formatiertem HTML (druckbar als PDF).
|
|
2967
|
+
|
|
2968
|
+
Args:
|
|
2969
|
+
- input_path (string): Pfad zur Markdown-Datei
|
|
2970
|
+
- output_path (string): Pfad zur HTML-Ausgabe
|
|
2971
|
+
- title (string, optional): Dokumenttitel
|
|
2972
|
+
|
|
2973
|
+
Erzeugt eigenstaendiges HTML mit CSS-Styling, druckbar als PDF ueber den Browser.`,
|
|
2974
|
+
inputSchema: {
|
|
2975
|
+
input_path: z.string().min(1).describe("Markdown-Datei"),
|
|
2976
|
+
output_path: z.string().min(1).describe("HTML-Ausgabe"),
|
|
2977
|
+
title: z.string().optional().describe("Dokumenttitel")
|
|
2978
|
+
},
|
|
2979
|
+
annotations: {
|
|
2980
|
+
readOnlyHint: false,
|
|
2981
|
+
destructiveHint: false,
|
|
2982
|
+
idempotentHint: true,
|
|
2983
|
+
openWorldHint: false
|
|
2984
|
+
}
|
|
2985
|
+
}, async (params) => {
|
|
2986
|
+
try {
|
|
2987
|
+
const inputPath = normalizePath(params.input_path);
|
|
2988
|
+
const outputPath = normalizePath(params.output_path);
|
|
2989
|
+
if (!await pathExists(inputPath)) {
|
|
2990
|
+
return { isError: true, content: [{ type: "text", text: `❌ Datei nicht gefunden: ${inputPath}` }] };
|
|
2991
|
+
}
|
|
2992
|
+
const md = await fs.readFile(inputPath, "utf-8");
|
|
2993
|
+
const title = params.title || path.basename(inputPath, '.md');
|
|
2994
|
+
let html = md
|
|
2995
|
+
.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>')
|
|
2996
|
+
.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>')
|
|
2997
|
+
.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>')
|
|
2998
|
+
.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>')
|
|
2999
|
+
.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>')
|
|
3000
|
+
.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>')
|
|
3001
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
3002
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
3003
|
+
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
|
3004
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
3005
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
|
3006
|
+
.replace(/^---$/gm, '<hr>')
|
|
3007
|
+
.replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>')
|
|
3008
|
+
.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
|
3009
|
+
.replace(/^\|(.+)\|$/gm, (match) => {
|
|
3010
|
+
const cells = match.split('|').filter(c => c.trim()).map(c => c.trim());
|
|
3011
|
+
if (cells.every(c => /^[-:]+$/.test(c)))
|
|
3012
|
+
return '';
|
|
3013
|
+
return '<tr>' + cells.map(c => `<td>${c}</td>`).join('') + '</tr>';
|
|
3014
|
+
})
|
|
3015
|
+
.replace(/(<tr>.*<\/tr>\n?)+/g, '<table>$&</table>')
|
|
3016
|
+
.replace(/^(?!<[hupolt]|<\/|$)(.+)$/gm, '<p>$1</p>');
|
|
3017
|
+
const fullHtml = `<!DOCTYPE html>
|
|
3018
|
+
<html lang="de">
|
|
3019
|
+
<head>
|
|
3020
|
+
<meta charset="UTF-8">
|
|
3021
|
+
<title>${title}</title>
|
|
3022
|
+
<style>
|
|
3023
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6; color: #333; }
|
|
3024
|
+
h1, h2, h3 { border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
|
3025
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
3026
|
+
pre { background: #f4f4f4; padding: 16px; border-radius: 6px; overflow-x: auto; }
|
|
3027
|
+
pre code { background: none; padding: 0; }
|
|
3028
|
+
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
|
3029
|
+
td, th { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
|
3030
|
+
tr:nth-child(even) { background: #f9f9f9; }
|
|
3031
|
+
hr { border: none; border-top: 2px solid #eee; margin: 2em 0; }
|
|
3032
|
+
@media print { body { max-width: none; margin: 0; } }
|
|
3033
|
+
</style>
|
|
3034
|
+
</head>
|
|
3035
|
+
<body>
|
|
3036
|
+
${html}
|
|
3037
|
+
</body>
|
|
3038
|
+
</html>`;
|
|
3039
|
+
const outDir = path.dirname(outputPath);
|
|
3040
|
+
if (!await pathExists(outDir))
|
|
3041
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
3042
|
+
await fs.writeFile(outputPath, fullHtml, "utf-8");
|
|
3043
|
+
const outStats = await fs.stat(outputPath);
|
|
3044
|
+
return {
|
|
3045
|
+
content: [{ type: "text", text: [
|
|
3046
|
+
`✅ **Markdown → HTML: ${path.basename(outputPath)}**`, '',
|
|
3047
|
+
`| | |`, `|---|---|`,
|
|
3048
|
+
`| Quelle | ${inputPath} |`,
|
|
3049
|
+
`| Ziel | ${outputPath} |`,
|
|
3050
|
+
`| Größe | ${formatFileSize(outStats.size)} |`, '',
|
|
3051
|
+
`💡 Öffne die HTML-Datei im Browser und drucke als PDF.`
|
|
3052
|
+
].join('\n') }]
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
catch (error) {
|
|
3056
|
+
return { isError: true, content: [{ type: "text", text: `❌ Fehler: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
3057
|
+
}
|
|
3058
|
+
});
|
|
3059
|
+
// ============================================================================
|
|
2026
3060
|
// Server Startup
|
|
2027
3061
|
// ============================================================================
|
|
2028
3062
|
async function main() {
|