@saber2pr/ai-agent 0.0.10 → 0.0.11

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,330 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setAllowedDirectories = setAllowedDirectories;
7
+ exports.getAllowedDirectories = getAllowedDirectories;
8
+ exports.formatSize = formatSize;
9
+ exports.normalizeLineEndings = normalizeLineEndings;
10
+ exports.createUnifiedDiff = createUnifiedDiff;
11
+ exports.validatePath = validatePath;
12
+ exports.getFileStats = getFileStats;
13
+ exports.readFileContent = readFileContent;
14
+ exports.writeFileContent = writeFileContent;
15
+ exports.applyFileEdits = applyFileEdits;
16
+ exports.tailFile = tailFile;
17
+ exports.headFile = headFile;
18
+ exports.searchFilesWithValidation = searchFilesWithValidation;
19
+ const promises_1 = __importDefault(require("fs/promises"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const crypto_1 = require("crypto");
22
+ const diff_1 = require("diff");
23
+ const minimatch_1 = require("minimatch");
24
+ const path_utils_js_1 = require("./path-utils.js");
25
+ const path_validation_js_1 = require("./path-validation.js");
26
+ // Global allowed directories - set by the main module
27
+ let allowedDirectories = [];
28
+ // Function to set allowed directories from the main module
29
+ function setAllowedDirectories(directories) {
30
+ allowedDirectories = [...directories];
31
+ }
32
+ // Function to get current allowed directories
33
+ function getAllowedDirectories() {
34
+ return [...allowedDirectories];
35
+ }
36
+ // Pure Utility Functions
37
+ function formatSize(bytes) {
38
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
39
+ if (bytes === 0)
40
+ return '0 B';
41
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
42
+ if (i < 0 || i === 0)
43
+ return `${bytes} ${units[0]}`;
44
+ const unitIndex = Math.min(i, units.length - 1);
45
+ return `${(bytes / Math.pow(1024, unitIndex)).toFixed(2)} ${units[unitIndex]}`;
46
+ }
47
+ function normalizeLineEndings(text) {
48
+ return text.replace(/\r\n/g, '\n');
49
+ }
50
+ function createUnifiedDiff(originalContent, newContent, filepath = 'file') {
51
+ // Ensure consistent line endings for diff
52
+ const normalizedOriginal = normalizeLineEndings(originalContent);
53
+ const normalizedNew = normalizeLineEndings(newContent);
54
+ return (0, diff_1.createTwoFilesPatch)(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified');
55
+ }
56
+ // Security & Validation Functions
57
+ async function validatePath(cwd, requestedPath) {
58
+ const expandedPath = (0, path_utils_js_1.expandHome)(requestedPath);
59
+ const absolute = path_1.default.isAbsolute(expandedPath)
60
+ ? path_1.default.resolve(expandedPath)
61
+ : path_1.default.resolve(cwd, expandedPath);
62
+ const normalizedRequested = (0, path_utils_js_1.normalizePath)(absolute);
63
+ // Security: Check if path is within allowed directories before any file operations
64
+ const isAllowed = (0, path_validation_js_1.isPathWithinAllowedDirectories)(normalizedRequested, allowedDirectories);
65
+ if (!isAllowed) {
66
+ throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
67
+ }
68
+ // Security: Handle symlinks by checking their real path to prevent symlink attacks
69
+ // This prevents attackers from creating symlinks that point outside allowed directories
70
+ try {
71
+ const realPath = await promises_1.default.realpath(absolute);
72
+ const normalizedReal = (0, path_utils_js_1.normalizePath)(realPath);
73
+ if (!(0, path_validation_js_1.isPathWithinAllowedDirectories)(normalizedReal, allowedDirectories)) {
74
+ throw new Error(`Access denied - symlink target outside allowed directories: ${realPath} not in ${allowedDirectories.join(', ')}`);
75
+ }
76
+ return realPath;
77
+ }
78
+ catch (error) {
79
+ // Security: For new files that don't exist yet, verify parent directory
80
+ // This ensures we can't create files in unauthorized locations
81
+ if (error.code === 'ENOENT') {
82
+ const parentDir = path_1.default.dirname(absolute);
83
+ try {
84
+ const realParentPath = await promises_1.default.realpath(parentDir);
85
+ const normalizedParent = (0, path_utils_js_1.normalizePath)(realParentPath);
86
+ if (!(0, path_validation_js_1.isPathWithinAllowedDirectories)(normalizedParent, allowedDirectories)) {
87
+ throw new Error(`Access denied - parent directory outside allowed directories: ${realParentPath} not in ${allowedDirectories.join(', ')}`);
88
+ }
89
+ return absolute;
90
+ }
91
+ catch {
92
+ throw new Error(`Parent directory does not exist: ${parentDir}`);
93
+ }
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ // File Operations
99
+ async function getFileStats(filePath) {
100
+ const stats = await promises_1.default.stat(filePath);
101
+ return {
102
+ size: stats.size,
103
+ created: stats.birthtime,
104
+ modified: stats.mtime,
105
+ accessed: stats.atime,
106
+ isDirectory: stats.isDirectory(),
107
+ isFile: stats.isFile(),
108
+ permissions: stats.mode.toString(8).slice(-3),
109
+ };
110
+ }
111
+ async function readFileContent(filePath, encoding = 'utf-8') {
112
+ return await promises_1.default.readFile(filePath, encoding);
113
+ }
114
+ async function writeFileContent(filePath, content) {
115
+ try {
116
+ // Security: 'wx' flag ensures exclusive creation - fails if file/symlink exists,
117
+ // preventing writes through pre-existing symlinks
118
+ await promises_1.default.writeFile(filePath, content, { encoding: "utf-8", flag: 'wx' });
119
+ }
120
+ catch (error) {
121
+ if (error.code === 'EEXIST') {
122
+ // Security: Use atomic rename to prevent race conditions where symlinks
123
+ // could be created between validation and write. Rename operations
124
+ // replace the target file atomically and don't follow symlinks.
125
+ const tempPath = `${filePath}.${(0, crypto_1.randomBytes)(16).toString('hex')}.tmp`;
126
+ try {
127
+ await promises_1.default.writeFile(tempPath, content, 'utf-8');
128
+ await promises_1.default.rename(tempPath, filePath);
129
+ }
130
+ catch (renameError) {
131
+ try {
132
+ await promises_1.default.unlink(tempPath);
133
+ }
134
+ catch { }
135
+ throw renameError;
136
+ }
137
+ }
138
+ else {
139
+ throw error;
140
+ }
141
+ }
142
+ }
143
+ async function applyFileEdits(filePath, edits, dryRun = false) {
144
+ var _a;
145
+ // Read file content and normalize line endings
146
+ const content = normalizeLineEndings(await promises_1.default.readFile(filePath, 'utf-8'));
147
+ // Apply edits sequentially
148
+ let modifiedContent = content;
149
+ for (const edit of edits) {
150
+ const normalizedOld = normalizeLineEndings(edit.oldText);
151
+ const normalizedNew = normalizeLineEndings(edit.newText);
152
+ // If exact match exists, use it
153
+ if (modifiedContent.includes(normalizedOld)) {
154
+ modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
155
+ continue;
156
+ }
157
+ // Otherwise, try line-by-line matching with flexibility for whitespace
158
+ const oldLines = normalizedOld.split('\n');
159
+ const contentLines = modifiedContent.split('\n');
160
+ let matchFound = false;
161
+ for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
162
+ const potentialMatch = contentLines.slice(i, i + oldLines.length);
163
+ // Compare lines with normalized whitespace
164
+ const isMatch = oldLines.every((oldLine, j) => {
165
+ const contentLine = potentialMatch[j];
166
+ return oldLine.trim() === contentLine.trim();
167
+ });
168
+ if (isMatch) {
169
+ // Preserve original indentation of first line
170
+ const originalIndent = ((_a = contentLines[i].match(/^\s*/)) === null || _a === void 0 ? void 0 : _a[0]) || '';
171
+ const newLines = normalizedNew.split('\n').map((line, j) => {
172
+ var _a, _b, _c;
173
+ if (j === 0)
174
+ return originalIndent + line.trimStart();
175
+ // For subsequent lines, try to preserve relative indentation
176
+ const oldIndent = ((_b = (_a = oldLines[j]) === null || _a === void 0 ? void 0 : _a.match(/^\s*/)) === null || _b === void 0 ? void 0 : _b[0]) || '';
177
+ const newIndent = ((_c = line.match(/^\s*/)) === null || _c === void 0 ? void 0 : _c[0]) || '';
178
+ if (oldIndent && newIndent) {
179
+ const relativeIndent = newIndent.length - oldIndent.length;
180
+ return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
181
+ }
182
+ return line;
183
+ });
184
+ contentLines.splice(i, oldLines.length, ...newLines);
185
+ modifiedContent = contentLines.join('\n');
186
+ matchFound = true;
187
+ break;
188
+ }
189
+ }
190
+ if (!matchFound) {
191
+ throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
192
+ }
193
+ }
194
+ // Create unified diff
195
+ const diff = createUnifiedDiff(content, modifiedContent, filePath);
196
+ // Format diff with appropriate number of backticks
197
+ let numBackticks = 3;
198
+ while (diff.includes('`'.repeat(numBackticks))) {
199
+ numBackticks++;
200
+ }
201
+ const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
202
+ if (!dryRun) {
203
+ // Security: Use atomic rename to prevent race conditions where symlinks
204
+ // could be created between validation and write. Rename operations
205
+ // replace the target file atomically and don't follow symlinks.
206
+ const tempPath = `${filePath}.${(0, crypto_1.randomBytes)(16).toString('hex')}.tmp`;
207
+ try {
208
+ await promises_1.default.writeFile(tempPath, modifiedContent, 'utf-8');
209
+ await promises_1.default.rename(tempPath, filePath);
210
+ }
211
+ catch (error) {
212
+ try {
213
+ await promises_1.default.unlink(tempPath);
214
+ }
215
+ catch { }
216
+ throw error;
217
+ }
218
+ }
219
+ return formattedDiff;
220
+ }
221
+ // Memory-efficient implementation to get the last N lines of a file
222
+ async function tailFile(filePath, numLines) {
223
+ const CHUNK_SIZE = 1024; // Read 1KB at a time
224
+ const stats = await promises_1.default.stat(filePath);
225
+ const fileSize = stats.size;
226
+ if (fileSize === 0)
227
+ return '';
228
+ // Open file for reading
229
+ const fileHandle = await promises_1.default.open(filePath, 'r');
230
+ try {
231
+ const lines = [];
232
+ let position = fileSize;
233
+ let chunk = Buffer.alloc(CHUNK_SIZE);
234
+ let linesFound = 0;
235
+ let remainingText = '';
236
+ // Read chunks from the end of the file until we have enough lines
237
+ while (position > 0 && linesFound < numLines) {
238
+ const size = Math.min(CHUNK_SIZE, position);
239
+ position -= size;
240
+ const { bytesRead } = await fileHandle.read(chunk, 0, size, position);
241
+ if (!bytesRead)
242
+ break;
243
+ // Get the chunk as a string and prepend any remaining text from previous iteration
244
+ const readData = chunk.slice(0, bytesRead).toString('utf-8');
245
+ const chunkText = readData + remainingText;
246
+ // Split by newlines and count
247
+ const chunkLines = normalizeLineEndings(chunkText).split('\n');
248
+ // If this isn't the end of the file, the first line is likely incomplete
249
+ // Save it to prepend to the next chunk
250
+ if (position > 0) {
251
+ remainingText = chunkLines[0];
252
+ chunkLines.shift(); // Remove the first (incomplete) line
253
+ }
254
+ // Add lines to our result (up to the number we need)
255
+ for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) {
256
+ lines.unshift(chunkLines[i]);
257
+ linesFound++;
258
+ }
259
+ }
260
+ return lines.join('\n');
261
+ }
262
+ finally {
263
+ await fileHandle.close();
264
+ }
265
+ }
266
+ // New function to get the first N lines of a file
267
+ async function headFile(filePath, numLines) {
268
+ const fileHandle = await promises_1.default.open(filePath, 'r');
269
+ try {
270
+ const lines = [];
271
+ let buffer = '';
272
+ let bytesRead = 0;
273
+ const chunk = Buffer.alloc(1024); // 1KB buffer
274
+ // Read chunks and count lines until we have enough or reach EOF
275
+ while (lines.length < numLines) {
276
+ const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead);
277
+ if (result.bytesRead === 0)
278
+ break; // End of file
279
+ bytesRead += result.bytesRead;
280
+ buffer += chunk.slice(0, result.bytesRead).toString('utf-8');
281
+ const newLineIndex = buffer.lastIndexOf('\n');
282
+ if (newLineIndex !== -1) {
283
+ const completeLines = buffer.slice(0, newLineIndex).split('\n');
284
+ buffer = buffer.slice(newLineIndex + 1);
285
+ for (const line of completeLines) {
286
+ lines.push(line);
287
+ if (lines.length >= numLines)
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ // If there is leftover content and we still need lines, add it
293
+ if (buffer.length > 0 && lines.length < numLines) {
294
+ lines.push(buffer);
295
+ }
296
+ return lines.join('\n');
297
+ }
298
+ finally {
299
+ await fileHandle.close();
300
+ }
301
+ }
302
+ async function searchFilesWithValidation(cwd, rootPath, pattern, allowedDirectories, options = {}) {
303
+ const { excludePatterns = [] } = options;
304
+ const results = [];
305
+ async function search(currentPath) {
306
+ const entries = await promises_1.default.readdir(currentPath, { withFileTypes: true });
307
+ for (const entry of entries) {
308
+ const fullPath = path_1.default.join(currentPath, entry.name);
309
+ try {
310
+ await validatePath(cwd, fullPath);
311
+ const relativePath = path_1.default.relative(rootPath, fullPath);
312
+ const shouldExclude = excludePatterns.some(excludePattern => (0, minimatch_1.minimatch)(relativePath, excludePattern, { dot: true }));
313
+ if (shouldExclude)
314
+ continue;
315
+ // Use glob matching for the search pattern
316
+ if ((0, minimatch_1.minimatch)(relativePath, pattern, { dot: true })) {
317
+ results.push(fullPath);
318
+ }
319
+ if (entry.isDirectory()) {
320
+ await search(fullPath);
321
+ }
322
+ }
323
+ catch {
324
+ continue;
325
+ }
326
+ }
327
+ }
328
+ await search(rootPath);
329
+ return results;
330
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Converts WSL or Unix-style Windows paths to Windows format
3
+ * @param p The path to convert
4
+ * @returns Converted Windows path
5
+ */
6
+ export declare function convertToWindowsPath(p: string): string;
7
+ /**
8
+ * Normalizes path by standardizing format while preserving OS-specific behavior
9
+ * @param p The path to normalize
10
+ * @returns Normalized path
11
+ */
12
+ export declare function normalizePath(p: string): string;
13
+ /**
14
+ * Expands home directory tildes in paths
15
+ * @param filepath The path to expand
16
+ * @returns Expanded path
17
+ */
18
+ export declare function expandHome(filepath: string): string;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.convertToWindowsPath = convertToWindowsPath;
7
+ exports.normalizePath = normalizePath;
8
+ exports.expandHome = expandHome;
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
11
+ /**
12
+ * Converts WSL or Unix-style Windows paths to Windows format
13
+ * @param p The path to convert
14
+ * @returns Converted Windows path
15
+ */
16
+ function convertToWindowsPath(p) {
17
+ // Handle WSL paths (/mnt/c/...)
18
+ // NEVER convert WSL paths - they are valid Linux paths that work with Node.js fs operations in WSL
19
+ // Converting them to Windows format (C:\...) breaks fs operations inside WSL
20
+ if (p.startsWith('/mnt/')) {
21
+ return p; // Leave WSL paths unchanged
22
+ }
23
+ // Handle Unix-style Windows paths (/c/...)
24
+ // Only convert when running on Windows
25
+ if (p.match(/^\/[a-zA-Z]\//) && process.platform === 'win32') {
26
+ const driveLetter = p.charAt(1).toUpperCase();
27
+ const pathPart = p.slice(2).replace(/\//g, '\\');
28
+ return `${driveLetter}:${pathPart}`;
29
+ }
30
+ // Handle standard Windows paths, ensuring backslashes
31
+ if (p.match(/^[a-zA-Z]:/)) {
32
+ return p.replace(/\//g, '\\');
33
+ }
34
+ // Leave non-Windows paths unchanged
35
+ return p;
36
+ }
37
+ /**
38
+ * Normalizes path by standardizing format while preserving OS-specific behavior
39
+ * @param p The path to normalize
40
+ * @returns Normalized path
41
+ */
42
+ function normalizePath(p) {
43
+ // Remove any surrounding quotes and whitespace
44
+ p = p.trim().replace(/^["']|["']$/g, '');
45
+ // Check if this is a Unix path that should not be converted
46
+ // WSL paths (/mnt/) should ALWAYS be preserved as they work correctly in WSL with Node.js fs
47
+ // Regular Unix paths should also be preserved
48
+ const isUnixPath = p.startsWith('/') && (
49
+ // Always preserve WSL paths (/mnt/c/, /mnt/d/, etc.)
50
+ p.match(/^\/mnt\/[a-z]\//i) ||
51
+ // On non-Windows platforms, treat all absolute paths as Unix paths
52
+ (process.platform !== 'win32') ||
53
+ // On Windows, preserve Unix paths that aren't Unix-style Windows paths (/c/, /d/, etc.)
54
+ (process.platform === 'win32' && !p.match(/^\/[a-zA-Z]\//)));
55
+ if (isUnixPath) {
56
+ // For Unix paths, just normalize without converting to Windows format
57
+ // Replace double slashes with single slashes and remove trailing slashes
58
+ return p.replace(/\/+/g, '/').replace(/(?<!^)\/$/, '');
59
+ }
60
+ // Convert Unix-style Windows paths (/c/, /d/) to Windows format if on Windows
61
+ // This function will now leave /mnt/ paths unchanged
62
+ p = convertToWindowsPath(p);
63
+ // Handle double backslashes, preserving leading UNC \\
64
+ if (p.startsWith('\\\\')) {
65
+ // For UNC paths, first normalize any excessive leading backslashes to exactly \\
66
+ // Then normalize double backslashes in the rest of the path
67
+ let uncPath = p;
68
+ // Replace multiple leading backslashes with exactly two
69
+ uncPath = uncPath.replace(/^\\{2,}/, '\\\\');
70
+ // Now normalize any remaining double backslashes in the rest of the path
71
+ const restOfPath = uncPath.substring(2).replace(/\\\\/g, '\\');
72
+ p = '\\\\' + restOfPath;
73
+ }
74
+ else {
75
+ // For non-UNC paths, normalize all double backslashes
76
+ p = p.replace(/\\\\/g, '\\');
77
+ }
78
+ // Use Node's path normalization, which handles . and .. segments
79
+ let normalized = path_1.default.normalize(p);
80
+ // Fix UNC paths after normalization (path.normalize can remove a leading backslash)
81
+ if (p.startsWith('\\\\') && !normalized.startsWith('\\\\')) {
82
+ normalized = '\\' + normalized;
83
+ }
84
+ // Handle Windows paths: convert slashes and ensure drive letter is capitalized
85
+ if (normalized.match(/^[a-zA-Z]:/)) {
86
+ let result = normalized.replace(/\//g, '\\');
87
+ // Capitalize drive letter if present
88
+ if (/^[a-z]:/.test(result)) {
89
+ result = result.charAt(0).toUpperCase() + result.slice(1);
90
+ }
91
+ return result;
92
+ }
93
+ // On Windows, convert forward slashes to backslashes for relative paths
94
+ // On Linux/Unix, preserve forward slashes
95
+ if (process.platform === 'win32') {
96
+ return normalized.replace(/\//g, '\\');
97
+ }
98
+ // On non-Windows platforms, keep the normalized path as-is
99
+ return normalized;
100
+ }
101
+ /**
102
+ * Expands home directory tildes in paths
103
+ * @param filepath The path to expand
104
+ * @returns Expanded path
105
+ */
106
+ function expandHome(filepath) {
107
+ if (filepath.startsWith('~/') || filepath === '~') {
108
+ return path_1.default.join(os_1.default.homedir(), filepath.slice(1));
109
+ }
110
+ return filepath;
111
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Checks if an absolute path is within any of the allowed directories.
3
+ *
4
+ * @param absolutePath - The absolute path to check (will be normalized)
5
+ * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized)
6
+ * @returns true if the path is within an allowed directory, false otherwise
7
+ * @throws Error if given relative paths after normalization
8
+ */
9
+ export declare function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isPathWithinAllowedDirectories = isPathWithinAllowedDirectories;
7
+ const path_1 = __importDefault(require("path"));
8
+ /**
9
+ * Checks if an absolute path is within any of the allowed directories.
10
+ *
11
+ * @param absolutePath - The absolute path to check (will be normalized)
12
+ * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized)
13
+ * @returns true if the path is within an allowed directory, false otherwise
14
+ * @throws Error if given relative paths after normalization
15
+ */
16
+ function isPathWithinAllowedDirectories(absolutePath, allowedDirectories) {
17
+ // Type validation
18
+ if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) {
19
+ return false;
20
+ }
21
+ // Reject empty inputs
22
+ if (!absolutePath || allowedDirectories.length === 0) {
23
+ return false;
24
+ }
25
+ // Reject null bytes (forbidden in paths)
26
+ if (absolutePath.includes('\x00')) {
27
+ return false;
28
+ }
29
+ // Normalize the input path
30
+ let normalizedPath;
31
+ try {
32
+ normalizedPath = path_1.default.resolve(path_1.default.normalize(absolutePath));
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ // Verify it's absolute after normalization
38
+ if (!path_1.default.isAbsolute(normalizedPath)) {
39
+ throw new Error('Path must be absolute after normalization');
40
+ }
41
+ // Check against each allowed directory
42
+ return allowedDirectories.some(dir => {
43
+ if (typeof dir !== 'string' || !dir) {
44
+ return false;
45
+ }
46
+ // Reject null bytes in allowed dirs
47
+ if (dir.includes('\x00')) {
48
+ return false;
49
+ }
50
+ // Normalize the allowed directory
51
+ let normalizedDir;
52
+ try {
53
+ normalizedDir = path_1.default.resolve(path_1.default.normalize(dir));
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ // Verify allowed directory is absolute after normalization
59
+ if (!path_1.default.isAbsolute(normalizedDir)) {
60
+ throw new Error('Allowed directories must be absolute paths after normalization');
61
+ }
62
+ // Check if normalizedPath is within normalizedDir
63
+ // Path is inside if it's the same or a subdirectory
64
+ if (normalizedPath === normalizedDir) {
65
+ return true;
66
+ }
67
+ // Special case for root directory to avoid double slash
68
+ // On Windows, we need to check if both paths are on the same drive
69
+ if (normalizedDir === path_1.default.sep) {
70
+ return normalizedPath.startsWith(path_1.default.sep);
71
+ }
72
+ // On Windows, also check for drive root (e.g., "C:\")
73
+ if (path_1.default.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) {
74
+ // Ensure both paths are on the same drive
75
+ const dirDrive = normalizedDir.charAt(0).toLowerCase();
76
+ const pathDrive = normalizedPath.charAt(0).toLowerCase();
77
+ return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\'));
78
+ }
79
+ return normalizedPath.startsWith(normalizedDir + path_1.default.sep);
80
+ });
81
+ }
@@ -0,0 +1,12 @@
1
+ import type { Root } from '@modelcontextprotocol/sdk/types.js';
2
+ /**
3
+ * Resolves requested root directories from MCP root specifications.
4
+ *
5
+ * Converts root URI specifications (file:// URIs or plain paths) into normalized
6
+ * directory paths, validating that each path exists and is a directory.
7
+ * Includes symlink resolution for security.
8
+ *
9
+ * @param requestedRoots - Array of root specifications with URI and optional name
10
+ * @returns Promise resolving to array of validated directory paths
11
+ */
12
+ export declare function getValidRootDirectories(requestedRoots: readonly Root[]): Promise<string[]>;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getValidRootDirectories = getValidRootDirectories;
7
+ const fs_1 = require("fs");
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_utils_js_1 = require("./path-utils.js");
11
+ /**
12
+ * Converts a root URI to a normalized directory path with basic security validation.
13
+ * @param rootUri - File URI (file://...) or plain directory path
14
+ * @returns Promise resolving to validated path or null if invalid
15
+ */
16
+ async function parseRootUri(rootUri) {
17
+ try {
18
+ const rawPath = rootUri.startsWith('file://') ? rootUri.slice(7) : rootUri;
19
+ const expandedPath = rawPath.startsWith('~/') || rawPath === '~'
20
+ ? path_1.default.join(os_1.default.homedir(), rawPath.slice(1))
21
+ : rawPath;
22
+ const absolutePath = path_1.default.resolve(expandedPath);
23
+ const resolvedPath = await fs_1.promises.realpath(absolutePath);
24
+ return (0, path_utils_js_1.normalizePath)(resolvedPath);
25
+ }
26
+ catch {
27
+ return null; // Path doesn't exist or other error
28
+ }
29
+ }
30
+ /**
31
+ * Formats error message for directory validation failures.
32
+ * @param dir - Directory path that failed validation
33
+ * @param error - Error that occurred during validation
34
+ * @param reason - Specific reason for failure
35
+ * @returns Formatted error message
36
+ */
37
+ function formatDirectoryError(dir, error, reason) {
38
+ if (reason) {
39
+ return `Skipping ${reason}: ${dir}`;
40
+ }
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ return `Skipping invalid directory: ${dir} due to error: ${message}`;
43
+ }
44
+ /**
45
+ * Resolves requested root directories from MCP root specifications.
46
+ *
47
+ * Converts root URI specifications (file:// URIs or plain paths) into normalized
48
+ * directory paths, validating that each path exists and is a directory.
49
+ * Includes symlink resolution for security.
50
+ *
51
+ * @param requestedRoots - Array of root specifications with URI and optional name
52
+ * @returns Promise resolving to array of validated directory paths
53
+ */
54
+ async function getValidRootDirectories(requestedRoots) {
55
+ const validatedDirectories = [];
56
+ for (const requestedRoot of requestedRoots) {
57
+ const resolvedPath = await parseRootUri(requestedRoot.uri);
58
+ if (!resolvedPath) {
59
+ console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible'));
60
+ continue;
61
+ }
62
+ try {
63
+ const stats = await fs_1.promises.stat(resolvedPath);
64
+ if (stats.isDirectory()) {
65
+ validatedDirectories.push(resolvedPath);
66
+ }
67
+ else {
68
+ console.error(formatDirectoryError(resolvedPath, undefined, 'non-directory root'));
69
+ }
70
+ }
71
+ catch (error) {
72
+ console.error(formatDirectoryError(resolvedPath, error));
73
+ }
74
+ }
75
+ return validatedDirectories;
76
+ }
@@ -0,0 +1 @@
1
+ export declare const getTsLspTools: (targetDir: string) => import("../../types/type").ToolInfo[];