@n0zer0d4y/vulcan-file-ops 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ import path from "path";
2
+ import os from 'os';
3
+ /**
4
+ * Converts WSL or Unix-style Windows paths to Windows format
5
+ * @param p The path to convert
6
+ * @returns Converted Windows path
7
+ */
8
+ export function convertToWindowsPath(p) {
9
+ // Handle WSL paths (/mnt/c/...)
10
+ if (p.startsWith('/mnt/')) {
11
+ const driveLetter = p.charAt(5).toUpperCase();
12
+ const pathPart = p.slice(6).replace(/\//g, '\\');
13
+ return `${driveLetter}:${pathPart}`;
14
+ }
15
+ // Handle Unix-style Windows paths (/c/...)
16
+ if (p.match(/^\/[a-zA-Z]\//)) {
17
+ const driveLetter = p.charAt(1).toUpperCase();
18
+ const pathPart = p.slice(2).replace(/\//g, '\\');
19
+ return `${driveLetter}:${pathPart}`;
20
+ }
21
+ // Handle standard Windows paths, ensuring backslashes
22
+ if (p.match(/^[a-zA-Z]:/)) {
23
+ return p.replace(/\//g, '\\');
24
+ }
25
+ // Leave non-Windows paths unchanged
26
+ return p;
27
+ }
28
+ /**
29
+ * Normalizes path by standardizing format while preserving OS-specific behavior
30
+ * @param p The path to normalize
31
+ * @returns Normalized path
32
+ */
33
+ export function normalizePath(p) {
34
+ // Remove any surrounding quotes and whitespace
35
+ p = p.trim().replace(/^["']|["']$/g, '');
36
+ // Check if this is a Unix path (starts with / but not a Windows or WSL path)
37
+ const isUnixPath = p.startsWith('/') &&
38
+ !p.match(/^\/mnt\/[a-z]\//i) &&
39
+ !p.match(/^\/[a-zA-Z]\//);
40
+ if (isUnixPath) {
41
+ // For Unix paths, just normalize without converting to Windows format
42
+ // Replace double slashes with single slashes and remove trailing slashes
43
+ return p.replace(/\/+/g, '/').replace(/\/+$/, '');
44
+ }
45
+ // Convert WSL or Unix-style Windows paths to Windows format
46
+ p = convertToWindowsPath(p);
47
+ // Handle double backslashes, preserving leading UNC \\
48
+ if (p.startsWith('\\\\')) {
49
+ // For UNC paths, first normalize any excessive leading backslashes to exactly \\
50
+ // Then normalize double backslashes in the rest of the path
51
+ let uncPath = p;
52
+ // Replace multiple leading backslashes with exactly two
53
+ uncPath = uncPath.replace(/^\\{2,}/, '\\\\');
54
+ // Now normalize any remaining double backslashes in the rest of the path
55
+ const restOfPath = uncPath.substring(2).replace(/\\\\/g, '\\');
56
+ p = '\\\\' + restOfPath;
57
+ }
58
+ else {
59
+ // For non-UNC paths, normalize all double backslashes
60
+ p = p.replace(/\\\\/g, '\\');
61
+ }
62
+ // Use Node's path normalization, which handles . and .. segments
63
+ let normalized = path.normalize(p);
64
+ // Fix UNC paths after normalization (path.normalize can remove a leading backslash)
65
+ if (p.startsWith('\\\\') && !normalized.startsWith('\\\\')) {
66
+ normalized = '\\' + normalized;
67
+ }
68
+ // Handle Windows paths: convert slashes and ensure drive letter is capitalized
69
+ if (normalized.match(/^[a-zA-Z]:/)) {
70
+ let result = normalized.replace(/\//g, '\\');
71
+ // Capitalize drive letter if present
72
+ if (/^[a-z]:/.test(result)) {
73
+ result = result.charAt(0).toUpperCase() + result.slice(1);
74
+ }
75
+ return result;
76
+ }
77
+ // For all other paths (including relative paths), convert forward slashes to backslashes
78
+ // This ensures relative paths like "some/relative/path" become "some\\relative\\path"
79
+ return normalized.replace(/\//g, '\\');
80
+ }
81
+ /**
82
+ * Expands home directory tildes in paths
83
+ * @param filepath The path to expand
84
+ * @returns Expanded path
85
+ */
86
+ export function expandHome(filepath) {
87
+ if (filepath.startsWith('~/') || filepath === '~') {
88
+ return path.join(os.homedir(), filepath.slice(1));
89
+ }
90
+ return filepath;
91
+ }
92
+ //# sourceMappingURL=path-utils.js.map
@@ -0,0 +1,76 @@
1
+ import path from 'path';
2
+ /**
3
+ * Checks if an absolute path is within any of the allowed directories.
4
+ *
5
+ * @param absolutePath - The absolute path to check (will be normalized)
6
+ * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized)
7
+ * @returns true if the path is within an allowed directory, false otherwise
8
+ * @throws Error if given relative paths after normalization
9
+ */
10
+ export function isPathWithinAllowedDirectories(absolutePath, allowedDirectories) {
11
+ // Type validation
12
+ if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) {
13
+ return false;
14
+ }
15
+ // Reject empty inputs
16
+ if (!absolutePath || allowedDirectories.length === 0) {
17
+ return false;
18
+ }
19
+ // Reject null bytes (forbidden in paths)
20
+ if (absolutePath.includes('\x00')) {
21
+ return false;
22
+ }
23
+ // Normalize the input path
24
+ let normalizedPath;
25
+ try {
26
+ normalizedPath = path.resolve(path.normalize(absolutePath));
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ // Verify it's absolute after normalization
32
+ if (!path.isAbsolute(normalizedPath)) {
33
+ throw new Error('Path must be absolute after normalization');
34
+ }
35
+ // Check against each allowed directory
36
+ return allowedDirectories.some(dir => {
37
+ if (typeof dir !== 'string' || !dir) {
38
+ return false;
39
+ }
40
+ // Reject null bytes in allowed dirs
41
+ if (dir.includes('\x00')) {
42
+ return false;
43
+ }
44
+ // Normalize the allowed directory
45
+ let normalizedDir;
46
+ try {
47
+ normalizedDir = path.resolve(path.normalize(dir));
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ // Verify allowed directory is absolute after normalization
53
+ if (!path.isAbsolute(normalizedDir)) {
54
+ throw new Error('Allowed directories must be absolute paths after normalization');
55
+ }
56
+ // Check if normalizedPath is within normalizedDir
57
+ // Path is inside if it's the same or a subdirectory
58
+ if (normalizedPath === normalizedDir) {
59
+ return true;
60
+ }
61
+ // Special case for root directory to avoid double slash
62
+ // On Windows, we need to check if both paths are on the same drive
63
+ if (normalizedDir === path.sep) {
64
+ return normalizedPath.startsWith(path.sep);
65
+ }
66
+ // On Windows, also check for drive root (e.g., "C:\")
67
+ if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) {
68
+ // Ensure both paths are on the same drive
69
+ const dirDrive = normalizedDir.charAt(0).toLowerCase();
70
+ const pathDrive = normalizedPath.charAt(0).toLowerCase();
71
+ return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\'));
72
+ }
73
+ return normalizedPath.startsWith(normalizedDir + path.sep);
74
+ });
75
+ }
76
+ //# sourceMappingURL=path-validation.js.map
@@ -0,0 +1,94 @@
1
+ import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
2
+ /**
3
+ * Create a new PDF document
4
+ */
5
+ export async function createPDF(options = {}) {
6
+ const pdfDoc = await PDFDocument.create();
7
+ if (options.title)
8
+ pdfDoc.setTitle(options.title);
9
+ if (options.author)
10
+ pdfDoc.setAuthor(options.author);
11
+ if (options.subject)
12
+ pdfDoc.setSubject(options.subject);
13
+ if (options.keywords)
14
+ pdfDoc.setKeywords(options.keywords);
15
+ return pdfDoc;
16
+ }
17
+ /**
18
+ * Save PDF to buffer
19
+ */
20
+ export async function savePDFToBuffer(pdfDoc) {
21
+ const pdfBytes = await pdfDoc.save();
22
+ return Buffer.from(pdfBytes);
23
+ }
24
+ /**
25
+ * Create simple text-based PDF from string content
26
+ * This is the main function called by write_file tool
27
+ */
28
+ export async function createSimpleTextPDF(content, options = {}) {
29
+ const pdfDoc = await createPDF(options);
30
+ const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
31
+ // A4 size: 595 x 842 points
32
+ const pageWidth = 595;
33
+ const pageHeight = 842;
34
+ const margin = 50;
35
+ const fontSize = 12;
36
+ const lineHeight = 14;
37
+ const maxWidth = pageWidth - 2 * margin;
38
+ let page = pdfDoc.addPage([pageWidth, pageHeight]);
39
+ let y = pageHeight - margin;
40
+ // Split content into lines
41
+ const lines = content.split("\n");
42
+ for (const line of lines) {
43
+ // Check if we need a new page
44
+ if (y < margin) {
45
+ page = pdfDoc.addPage([pageWidth, pageHeight]);
46
+ y = pageHeight - margin;
47
+ }
48
+ // Handle empty lines
49
+ if (line.trim() === "") {
50
+ y -= lineHeight;
51
+ continue;
52
+ }
53
+ // Simple word wrapping
54
+ const words = line.split(" ");
55
+ let currentLine = "";
56
+ for (const word of words) {
57
+ const testLine = currentLine + (currentLine ? " " : "") + word;
58
+ const textWidth = font.widthOfTextAtSize(testLine, fontSize);
59
+ if (textWidth > maxWidth && currentLine) {
60
+ // Draw current line and start new one
61
+ page.drawText(currentLine, {
62
+ x: margin,
63
+ y,
64
+ size: fontSize,
65
+ font,
66
+ color: rgb(0, 0, 0),
67
+ });
68
+ y -= lineHeight;
69
+ currentLine = word;
70
+ // Check if we need a new page
71
+ if (y < margin) {
72
+ page = pdfDoc.addPage([pageWidth, pageHeight]);
73
+ y = pageHeight - margin;
74
+ }
75
+ }
76
+ else {
77
+ currentLine = testLine;
78
+ }
79
+ }
80
+ // Draw remaining text
81
+ if (currentLine) {
82
+ page.drawText(currentLine, {
83
+ x: margin,
84
+ y,
85
+ size: fontSize,
86
+ font,
87
+ color: rgb(0, 0, 0),
88
+ });
89
+ y -= lineHeight;
90
+ }
91
+ }
92
+ return await savePDFToBuffer(pdfDoc);
93
+ }
94
+ //# sourceMappingURL=pdf-writer.js.map
@@ -0,0 +1,71 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { normalizePath } from './path-utils.js';
5
+ /**
6
+ * Converts a root URI to a normalized directory path with basic security validation.
7
+ * @param rootUri - File URI (file://...) or plain directory path
8
+ * @returns Promise resolving to validated path or null if invalid
9
+ */
10
+ async function parseRootUri(rootUri) {
11
+ try {
12
+ const rawPath = rootUri.startsWith('file://') ? rootUri.slice(7) : rootUri;
13
+ const expandedPath = rawPath.startsWith('~/') || rawPath === '~'
14
+ ? path.join(os.homedir(), rawPath.slice(1))
15
+ : rawPath;
16
+ const absolutePath = path.resolve(expandedPath);
17
+ const resolvedPath = await fs.realpath(absolutePath);
18
+ return normalizePath(resolvedPath);
19
+ }
20
+ catch {
21
+ return null; // Path doesn't exist or other error
22
+ }
23
+ }
24
+ /**
25
+ * Formats error message for directory validation failures.
26
+ * @param dir - Directory path that failed validation
27
+ * @param error - Error that occurred during validation
28
+ * @param reason - Specific reason for failure
29
+ * @returns Formatted error message
30
+ */
31
+ function formatDirectoryError(dir, error, reason) {
32
+ if (reason) {
33
+ return `Skipping ${reason}: ${dir}`;
34
+ }
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ return `Skipping invalid directory: ${dir} due to error: ${message}`;
37
+ }
38
+ /**
39
+ * Resolves requested root directories from MCP root specifications.
40
+ *
41
+ * Converts root URI specifications (file:// URIs or plain paths) into normalized
42
+ * directory paths, validating that each path exists and is a directory.
43
+ * Includes symlink resolution for security.
44
+ *
45
+ * @param requestedRoots - Array of root specifications with URI and optional name
46
+ * @returns Promise resolving to array of validated directory paths
47
+ */
48
+ export async function getValidRootDirectories(requestedRoots) {
49
+ const validatedDirectories = [];
50
+ for (const requestedRoot of requestedRoots) {
51
+ const resolvedPath = await parseRootUri(requestedRoot.uri);
52
+ if (!resolvedPath) {
53
+ console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible'));
54
+ continue;
55
+ }
56
+ try {
57
+ const stats = await fs.stat(resolvedPath);
58
+ if (stats.isDirectory()) {
59
+ validatedDirectories.push(resolvedPath);
60
+ }
61
+ else {
62
+ console.error(formatDirectoryError(resolvedPath, undefined, 'non-directory root'));
63
+ }
64
+ }
65
+ catch (error) {
66
+ console.error(formatDirectoryError(resolvedPath, error));
67
+ }
68
+ }
69
+ return validatedDirectories;
70
+ }
71
+ //# sourceMappingURL=roots-utils.js.map
@@ -0,0 +1,77 @@
1
+ import { spawn } from "child_process";
2
+ import { getShellConfig } from "./command-validation.js";
3
+ /**
4
+ * Execute a shell command with proper security and resource management
5
+ */
6
+ export async function executeShellCommand(command, options = {}) {
7
+ const { workdir = process.cwd(), timeout = 30000, env } = options;
8
+ const shellConfig = getShellConfig();
9
+ return new Promise((resolve) => {
10
+ let stdout = "";
11
+ let stderr = "";
12
+ let timedOut = false;
13
+ let resolved = false;
14
+ // Spawn the shell process
15
+ const child = spawn(shellConfig.shell, [...shellConfig.args, command], {
16
+ cwd: workdir,
17
+ env: { ...process.env, ...env },
18
+ shell: false, // Already using explicit shell
19
+ windowsHide: true,
20
+ });
21
+ // Set up timeout
22
+ const timeoutHandle = setTimeout(() => {
23
+ if (!resolved) {
24
+ timedOut = true;
25
+ child.kill("SIGTERM");
26
+ // Force kill after 5 seconds if SIGTERM doesn't work
27
+ setTimeout(() => {
28
+ if (!resolved) {
29
+ child.kill("SIGKILL");
30
+ }
31
+ }, 5000);
32
+ }
33
+ }, timeout);
34
+ // Capture stdout
35
+ if (child.stdout) {
36
+ child.stdout.on("data", (data) => {
37
+ stdout += data.toString();
38
+ });
39
+ }
40
+ // Capture stderr
41
+ if (child.stderr) {
42
+ child.stderr.on("data", (data) => {
43
+ stderr += data.toString();
44
+ });
45
+ }
46
+ // Handle process completion
47
+ child.on("close", (exitCode, signal) => {
48
+ if (resolved)
49
+ return;
50
+ resolved = true;
51
+ clearTimeout(timeoutHandle);
52
+ resolve({
53
+ stdout: stdout.trim(),
54
+ stderr: stderr.trim(),
55
+ exitCode,
56
+ signal,
57
+ timedOut,
58
+ });
59
+ });
60
+ // Handle errors
61
+ child.on("error", (error) => {
62
+ if (resolved)
63
+ return;
64
+ resolved = true;
65
+ clearTimeout(timeoutHandle);
66
+ resolve({
67
+ stdout: stdout.trim(),
68
+ stderr: stderr.trim(),
69
+ exitCode: null,
70
+ signal: null,
71
+ timedOut: false,
72
+ error,
73
+ });
74
+ });
75
+ });
76
+ }
77
+ //# sourceMappingURL=shell-execution.js.map
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@n0zer0d4y/vulcan-file-ops",
3
+ "version": "1.0.1",
4
+ "description": "MCP server that gives Claude Desktop and other AI assistants filesystem superpowers—read, write, edit, and manage files like AI coding assistants",
5
+ "license": "MIT",
6
+ "author": "Lloyd Barcatan",
7
+ "homepage": "https://github.com/n0zer0d4y/vulcan-file-ops",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/n0zer0d4y/vulcan-file-ops.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/n0zer0d4y/vulcan-file-ops/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "vulcan-file-ops": "dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist/cli.js",
21
+ "dist/index.js",
22
+ "dist/server/index.js",
23
+ "dist/tools/*.js",
24
+ "dist/utils/*.js",
25
+ "dist/types/index.js",
26
+ "README.md",
27
+ "LICENSE",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc && shx chmod +x dist/*.js",
32
+ "prepare": "npm run build",
33
+ "prepublishOnly": "npm test",
34
+ "watch": "tsc --watch",
35
+ "test": "jest --config=jest.config.cjs",
36
+ "test:coverage": "jest --config=jest.config.cjs --coverage",
37
+ "start": "node dist/index.js",
38
+ "lint:json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\" && echo \"package.json is valid JSON\"",
39
+ "lint": "npm run lint:json"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.20.0",
43
+ "@turbodocx/html-to-docx": "^1.16.0",
44
+ "diff": "^8.0.2",
45
+ "docx": "^9.5.1",
46
+ "dotenv": "^17.2.3",
47
+ "glob": "^11.0.3",
48
+ "html-to-pdfmake": "^2.5.31",
49
+ "jsdom": "^27.0.1",
50
+ "mammoth": "^1.11.0",
51
+ "minimatch": "^10.0.3",
52
+ "officeparser": "^5.2.1",
53
+ "pdf-lib": "^1.17.1",
54
+ "pdf-parse": "^2.3.0",
55
+ "pdfmake": "^0.2.20",
56
+ "zod-to-json-schema": "^3.24.6"
57
+ },
58
+ "devDependencies": {
59
+ "@jest/globals": "^30.2.0",
60
+ "@types/html-to-docx": "^1.8.0",
61
+ "@types/jest": "^30.0.0",
62
+ "@types/jsdom": "^27.0.0",
63
+ "@types/minimatch": "^5.1.2",
64
+ "@types/node": "^22",
65
+ "@types/pdf-parse": "^1.1.5",
66
+ "@types/pdfmake": "^0.2.12",
67
+ "jest": "^30.2.0",
68
+ "shx": "^0.3.4",
69
+ "ts-jest": "^29.4.5",
70
+ "ts-node": "^10.9.2",
71
+ "typescript": "^5.9.3"
72
+ }
73
+ }