@wonderwhy-er/desktop-commander 0.1.34 → 0.1.36
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/LICENSE +2 -2
- package/README.md +186 -56
- package/dist/command-manager.d.ts +1 -7
- package/dist/command-manager.js +31 -50
- package/dist/config-manager.d.ts +28 -16
- package/dist/config-manager.js +124 -189
- package/dist/config.d.ts +2 -2
- package/dist/config.js +7 -4
- package/dist/error-handlers.js +4 -0
- package/dist/handlers/edit-search-handlers.d.ts +3 -1
- package/dist/handlers/edit-search-handlers.js +9 -19
- package/dist/handlers/filesystem-handlers.d.ts +0 -4
- package/dist/handlers/filesystem-handlers.js +11 -19
- package/dist/handlers/index.d.ts +0 -1
- package/dist/handlers/index.js +0 -1
- package/dist/index.js +19 -4
- package/dist/polyform-license-src/edit/edit.d.ts +15 -0
- package/dist/polyform-license-src/edit/edit.js +163 -0
- package/dist/polyform-license-src/edit/fuzzySearch.d.ts +30 -0
- package/dist/polyform-license-src/edit/fuzzySearch.js +121 -0
- package/dist/polyform-license-src/edit/handlers.d.ts +16 -0
- package/dist/polyform-license-src/edit/handlers.js +24 -0
- package/dist/polyform-license-src/edit/index.d.ts +12 -0
- package/dist/polyform-license-src/edit/index.js +13 -0
- package/dist/polyform-license-src/edit/schemas.d.ts +25 -0
- package/dist/polyform-license-src/edit/schemas.js +16 -0
- package/dist/polyform-license-src/index.d.ts +9 -0
- package/dist/polyform-license-src/index.js +10 -0
- package/dist/sandbox/index.d.ts +9 -0
- package/dist/sandbox/index.js +50 -0
- package/dist/sandbox/mac-sandbox.d.ts +19 -0
- package/dist/sandbox/mac-sandbox.js +174 -0
- package/dist/server.js +181 -176
- package/dist/setup-claude-server.js +554 -244
- package/dist/terminal-manager.d.ts +1 -1
- package/dist/terminal-manager.js +22 -3
- package/dist/tools/config.d.ts +0 -58
- package/dist/tools/config.js +44 -107
- package/dist/tools/debug-path.d.ts +1 -0
- package/dist/tools/debug-path.js +44 -0
- package/dist/tools/edit.d.ts +8 -6
- package/dist/tools/edit.js +165 -35
- package/dist/tools/execute.js +6 -6
- package/dist/tools/filesystem-fixed.d.ts +22 -0
- package/dist/tools/filesystem-fixed.js +176 -0
- package/dist/tools/filesystem.d.ts +4 -6
- package/dist/tools/filesystem.js +157 -87
- package/dist/tools/fuzzySearch.d.ts +22 -0
- package/dist/tools/fuzzySearch.js +113 -0
- package/dist/tools/pdf-reader.d.ts +13 -0
- package/dist/tools/pdf-reader.js +214 -0
- package/dist/tools/schemas.d.ts +29 -19
- package/dist/tools/schemas.js +15 -8
- package/dist/tools/search.js +5 -4
- package/dist/utils/capture.d.ts +15 -0
- package/dist/utils/capture.js +175 -0
- package/dist/utils/withTimeout.d.ts +11 -0
- package/dist/utils/withTimeout.js +52 -0
- package/dist/utils.d.ts +15 -1
- package/dist/utils.js +174 -41
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
if (returnMetadata === true) {
|
|
2
|
+
return { content, mimeType, isImage };
|
|
3
|
+
}
|
|
4
|
+
else {
|
|
5
|
+
return content;
|
|
6
|
+
}
|
|
7
|
+
try { }
|
|
8
|
+
catch (error) {
|
|
9
|
+
// If UTF-8 reading fails, treat as binary and return base64 but still as text
|
|
10
|
+
const buffer = await fs.readFile(validPath);
|
|
11
|
+
const content = `Binary file content (base64 encoded):\n${buffer.toString('base64')}`;
|
|
12
|
+
if (returnMetadata === true) {
|
|
13
|
+
return { content, mimeType: 'text/plain', isImage: false };
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
return content;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
;
|
|
20
|
+
// Execute with timeout
|
|
21
|
+
const result = await withTimeout(readOperation(), FILE_READ_TIMEOUT, `Read file operation for ${filePath}`, returnMetadata ?
|
|
22
|
+
{ content: `Operation timed out after ${FILE_READ_TIMEOUT / 1000} seconds`, mimeType: 'text/plain', isImage: false } :
|
|
23
|
+
`Operation timed out after ${FILE_READ_TIMEOUT / 1000} seconds`);
|
|
24
|
+
return result;
|
|
25
|
+
/**
|
|
26
|
+
* Read a file from either the local filesystem or a URL
|
|
27
|
+
* @param filePath Path to the file or URL
|
|
28
|
+
* @param returnMetadata Whether to return metadata with the content
|
|
29
|
+
* @param isUrl Whether the path is a URL
|
|
30
|
+
* @returns File content or file result with metadata
|
|
31
|
+
*/
|
|
32
|
+
export async function readFile(filePath, returnMetadata, isUrl) {
|
|
33
|
+
return isUrl
|
|
34
|
+
? readFileFromUrl(filePath, returnMetadata)
|
|
35
|
+
: readFileFromDisk(filePath, returnMetadata);
|
|
36
|
+
}
|
|
37
|
+
export async function writeFile(filePath, content) {
|
|
38
|
+
// Expand home directory in filePath before validation
|
|
39
|
+
const expandedFilePath = expandHome(filePath);
|
|
40
|
+
const validPath = await validatePath(expandedFilePath);
|
|
41
|
+
// Check if the path validation returned an error
|
|
42
|
+
if (validPath.startsWith('__ERROR__:')) {
|
|
43
|
+
throw new Error(validPath.substring('__ERROR__:'.length).trim());
|
|
44
|
+
}
|
|
45
|
+
await fs.writeFile(validPath, content, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
export async function readMultipleFiles(paths) {
|
|
48
|
+
return Promise.all(paths.map(async (filePath) => {
|
|
49
|
+
try {
|
|
50
|
+
// Expand home directory in filePath before validation
|
|
51
|
+
const expandedFilePath = expandHome(filePath);
|
|
52
|
+
const validPath = await validatePath(expandedFilePath);
|
|
53
|
+
// Check if the path validation returned an error
|
|
54
|
+
if (validPath.startsWith('__ERROR__:')) {
|
|
55
|
+
return {
|
|
56
|
+
path: filePath,
|
|
57
|
+
error: validPath.substring('__ERROR__:'.length).trim()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const fileResult = await readFile(validPath, true);
|
|
61
|
+
return {
|
|
62
|
+
path: filePath,
|
|
63
|
+
content: typeof fileResult === 'string' ? fileResult : fileResult.content,
|
|
64
|
+
mimeType: typeof fileResult === 'string' ? "text/plain" : fileResult.mimeType,
|
|
65
|
+
isImage: typeof fileResult === 'string' ? false : fileResult.isImage
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return {
|
|
71
|
+
path: filePath,
|
|
72
|
+
error: errorMessage
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
export async function createDirectory(dirPath) {
|
|
78
|
+
// Expand home directory in dirPath before validation
|
|
79
|
+
const expandedDirPath = expandHome(dirPath);
|
|
80
|
+
const validPath = await validatePath(expandedDirPath);
|
|
81
|
+
// Check if the path validation returned an error
|
|
82
|
+
if (validPath.startsWith('__ERROR__:')) {
|
|
83
|
+
throw new Error(validPath.substring('__ERROR__:'.length).trim());
|
|
84
|
+
}
|
|
85
|
+
await fs.mkdir(validPath, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
export async function listDirectory(dirPath) {
|
|
88
|
+
console.log(`List directory called with: ${dirPath}`);
|
|
89
|
+
// Handle tilde expansion directly here as well
|
|
90
|
+
const expandedDirPath = expandHome(dirPath);
|
|
91
|
+
console.log(`Expanded dir path: ${expandedDirPath}`);
|
|
92
|
+
const validPath = await validatePath(expandedDirPath);
|
|
93
|
+
console.log(`Valid path: ${validPath}`);
|
|
94
|
+
// Check if the path starts with an error indicator
|
|
95
|
+
if (validPath.startsWith('__ERROR__:')) {
|
|
96
|
+
console.error(`Path validation error: ${validPath}`);
|
|
97
|
+
return [`Error: ${validPath.substring('__ERROR__:'.length).trim()}`];
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
|
101
|
+
return entries.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
105
|
+
console.error(`Error reading directory: ${errorMessage}`);
|
|
106
|
+
return [`Error reading directory: ${errorMessage}`];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function moveFile(sourcePath, destinationPath) {
|
|
110
|
+
// Expand home directory in both paths before validation
|
|
111
|
+
const expandedSourcePath = expandHome(sourcePath);
|
|
112
|
+
const expandedDestPath = expandHome(destinationPath);
|
|
113
|
+
const validSourcePath = await validatePath(expandedSourcePath);
|
|
114
|
+
const validDestPath = await validatePath(expandedDestPath);
|
|
115
|
+
// Check if either path validation returned an error
|
|
116
|
+
if (validSourcePath.startsWith('__ERROR__:')) {
|
|
117
|
+
throw new Error(validSourcePath.substring('__ERROR__:'.length).trim());
|
|
118
|
+
}
|
|
119
|
+
if (validDestPath.startsWith('__ERROR__:')) {
|
|
120
|
+
throw new Error(validDestPath.substring('__ERROR__:'.length).trim());
|
|
121
|
+
}
|
|
122
|
+
await fs.rename(validSourcePath, validDestPath);
|
|
123
|
+
}
|
|
124
|
+
export async function searchFiles(rootPath, pattern) {
|
|
125
|
+
const results = [];
|
|
126
|
+
async function search(currentPath) {
|
|
127
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
130
|
+
try {
|
|
131
|
+
// No need to expand home directory here as we're generating full paths
|
|
132
|
+
// from the already validated rootPath
|
|
133
|
+
await validatePath(fullPath);
|
|
134
|
+
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
|
|
135
|
+
results.push(fullPath);
|
|
136
|
+
}
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
await search(fullPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Expand home directory in rootPath before validation
|
|
147
|
+
const expandedRootPath = expandHome(rootPath);
|
|
148
|
+
const validPath = await validatePath(expandedRootPath);
|
|
149
|
+
// Check if the path validation returned an error
|
|
150
|
+
if (validPath.startsWith('__ERROR__:')) {
|
|
151
|
+
throw new Error(validPath.substring('__ERROR__:'.length).trim());
|
|
152
|
+
}
|
|
153
|
+
await search(validPath);
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
export async function getFileInfo(filePath) {
|
|
157
|
+
// Expand home directory in filePath before validation
|
|
158
|
+
const expandedFilePath = expandHome(filePath);
|
|
159
|
+
const validPath = await validatePath(expandedFilePath);
|
|
160
|
+
// Check if the path validation returned an error
|
|
161
|
+
if (validPath.startsWith('__ERROR__:')) {
|
|
162
|
+
throw new Error(validPath.substring('__ERROR__:'.length).trim());
|
|
163
|
+
}
|
|
164
|
+
const stats = await fs.stat(validPath);
|
|
165
|
+
return {
|
|
166
|
+
size: stats.size,
|
|
167
|
+
created: stats.birthtime,
|
|
168
|
+
modified: stats.mtime,
|
|
169
|
+
accessed: stats.atime,
|
|
170
|
+
isDirectory: stats.isDirectory(),
|
|
171
|
+
isFile: stats.isFile(),
|
|
172
|
+
permissions: stats.mode.toString(8).slice(-3),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// This function has been replaced with configManager.getConfig()
|
|
176
|
+
// Use get_config tool to retrieve allowedDirectories
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @param requestedPath The path to validate
|
|
7
7
|
* @returns Promise<string> The validated path
|
|
8
|
-
* @throws Error if the path or its parent directories don't exist
|
|
8
|
+
* @throws Error if the path or its parent directories don't exist or if the path is not allowed
|
|
9
9
|
*/
|
|
10
10
|
export declare function validatePath(requestedPath: string): Promise<string>;
|
|
11
11
|
export interface FileResult {
|
|
@@ -16,17 +16,16 @@ export interface FileResult {
|
|
|
16
16
|
/**
|
|
17
17
|
* Read file content from a URL
|
|
18
18
|
* @param url URL to fetch content from
|
|
19
|
-
* @param returnMetadata Whether to return metadata with the content
|
|
20
19
|
* @returns File content or file result with metadata
|
|
21
20
|
*/
|
|
22
|
-
export declare function readFileFromUrl(url: string
|
|
21
|
+
export declare function readFileFromUrl(url: string): Promise<FileResult>;
|
|
23
22
|
/**
|
|
24
23
|
* Read file content from the local filesystem
|
|
25
24
|
* @param filePath Path to the file
|
|
26
25
|
* @param returnMetadata Whether to return metadata with the content
|
|
27
26
|
* @returns File content or file result with metadata
|
|
28
27
|
*/
|
|
29
|
-
export declare function readFileFromDisk(filePath: string
|
|
28
|
+
export declare function readFileFromDisk(filePath: string): Promise<FileResult>;
|
|
30
29
|
/**
|
|
31
30
|
* Read a file from either the local filesystem or a URL
|
|
32
31
|
* @param filePath Path to the file or URL
|
|
@@ -34,7 +33,7 @@ export declare function readFileFromDisk(filePath: string, returnMetadata?: bool
|
|
|
34
33
|
* @param isUrl Whether the path is a URL
|
|
35
34
|
* @returns File content or file result with metadata
|
|
36
35
|
*/
|
|
37
|
-
export declare function readFile(filePath: string,
|
|
36
|
+
export declare function readFile(filePath: string, isUrl?: boolean): Promise<FileResult>;
|
|
38
37
|
export declare function writeFile(filePath: string, content: string): Promise<void>;
|
|
39
38
|
export interface MultiFileResult {
|
|
40
39
|
path: string;
|
|
@@ -49,4 +48,3 @@ export declare function listDirectory(dirPath: string): Promise<string[]>;
|
|
|
49
48
|
export declare function moveFile(sourcePath: string, destinationPath: string): Promise<void>;
|
|
50
49
|
export declare function searchFiles(rootPath: string, pattern: string): Promise<string[]>;
|
|
51
50
|
export declare function getFileInfo(filePath: string): Promise<Record<string, any>>;
|
|
52
|
-
export declare function listAllowedDirectories(): string[];
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -2,22 +2,36 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fetch from 'cross-fetch';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
import { capture } from '../utils/capture.js';
|
|
6
|
+
import { withTimeout } from '../utils/withTimeout.js';
|
|
7
|
+
import { configManager } from '../config-manager.js';
|
|
8
|
+
// Initialize allowed directories from configuration
|
|
9
|
+
async function getAllowedDirs() {
|
|
10
|
+
try {
|
|
11
|
+
let allowedDirectories;
|
|
12
|
+
const config = await configManager.getConfig();
|
|
13
|
+
if (config.allowedDirectories && Array.isArray(config.allowedDirectories)) {
|
|
14
|
+
allowedDirectories = config.allowedDirectories;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// Fall back to default directories if not configured
|
|
18
|
+
allowedDirectories = [
|
|
19
|
+
os.homedir() // User's home directory
|
|
20
|
+
];
|
|
21
|
+
// Update config with default
|
|
22
|
+
await configManager.setValue('allowedDirectories', allowedDirectories);
|
|
23
|
+
}
|
|
24
|
+
return allowedDirectories;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error('Failed to initialize allowed directories:', error);
|
|
28
|
+
// Keep the default permissive path
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
18
32
|
// Normalize all paths consistently
|
|
19
33
|
function normalizePath(p) {
|
|
20
|
-
return path.normalize(p).toLowerCase();
|
|
34
|
+
return path.normalize(expandHome(p)).toLowerCase();
|
|
21
35
|
}
|
|
22
36
|
function expandHome(filepath) {
|
|
23
37
|
if (filepath.startsWith('~/') || filepath === '~') {
|
|
@@ -49,6 +63,47 @@ async function validateParentDirectories(directoryPath) {
|
|
|
49
63
|
return validateParentDirectories(parentDir);
|
|
50
64
|
}
|
|
51
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Checks if a path is within any of the allowed directories
|
|
68
|
+
*
|
|
69
|
+
* @param pathToCheck Path to check
|
|
70
|
+
* @returns boolean True if path is allowed
|
|
71
|
+
*/
|
|
72
|
+
async function isPathAllowed(pathToCheck) {
|
|
73
|
+
// If root directory is allowed, all paths are allowed
|
|
74
|
+
const allowedDirectories = await getAllowedDirs();
|
|
75
|
+
if (allowedDirectories.includes('/') || allowedDirectories.length === 0) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
let normalizedPathToCheck = normalizePath(pathToCheck);
|
|
79
|
+
if (normalizedPathToCheck.slice(-1) === path.sep) {
|
|
80
|
+
normalizedPathToCheck = normalizedPathToCheck.slice(0, -1);
|
|
81
|
+
}
|
|
82
|
+
// Check if the path is within any allowed directory
|
|
83
|
+
const isAllowed = allowedDirectories.some(allowedDir => {
|
|
84
|
+
let normalizedAllowedDir = normalizePath(allowedDir);
|
|
85
|
+
if (normalizedAllowedDir.slice(-1) === path.sep) {
|
|
86
|
+
normalizedAllowedDir = normalizedAllowedDir.slice(0, -1);
|
|
87
|
+
}
|
|
88
|
+
// Check if path is exactly the allowed directory
|
|
89
|
+
if (normalizedPathToCheck === normalizedAllowedDir) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// Check if path is a subdirectory of the allowed directory
|
|
93
|
+
// Make sure to add a separator to prevent partial directory name matches
|
|
94
|
+
// e.g. /home/user vs /home/username
|
|
95
|
+
const subdirCheck = normalizedPathToCheck.startsWith(normalizedAllowedDir + path.sep);
|
|
96
|
+
if (subdirCheck) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
// If allowed directory is the root (C:\ on Windows), allow access to the entire drive
|
|
100
|
+
if (normalizedAllowedDir === 'c:' && process.platform === 'win32') {
|
|
101
|
+
return normalizedPathToCheck.startsWith('c:');
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
});
|
|
105
|
+
return isAllowed;
|
|
106
|
+
}
|
|
52
107
|
/**
|
|
53
108
|
* Validates a path to ensure it can be accessed or created.
|
|
54
109
|
* For existing paths, returns the real path (resolving symlinks).
|
|
@@ -56,7 +111,7 @@ async function validateParentDirectories(directoryPath) {
|
|
|
56
111
|
*
|
|
57
112
|
* @param requestedPath The path to validate
|
|
58
113
|
* @returns Promise<string> The validated path
|
|
59
|
-
* @throws Error if the path or its parent directories don't exist
|
|
114
|
+
* @throws Error if the path or its parent directories don't exist or if the path is not allowed
|
|
60
115
|
*/
|
|
61
116
|
export async function validatePath(requestedPath) {
|
|
62
117
|
const PATH_VALIDATION_TIMEOUT = 10000; // 10 seconds timeout
|
|
@@ -67,6 +122,14 @@ export async function validatePath(requestedPath) {
|
|
|
67
122
|
const absolute = path.isAbsolute(expandedPath)
|
|
68
123
|
? path.resolve(expandedPath)
|
|
69
124
|
: path.resolve(process.cwd(), expandedPath);
|
|
125
|
+
// Check if path is allowed
|
|
126
|
+
if (!(await isPathAllowed(absolute))) {
|
|
127
|
+
capture('server_path_validation_error', {
|
|
128
|
+
error: 'Path not allowed',
|
|
129
|
+
allowedDirsCount: (await getAllowedDirs()).length
|
|
130
|
+
});
|
|
131
|
+
throw new Error(`Path not allowed: ${requestedPath}. Must be within one of these directories: ${(await getAllowedDirs()).join(', ')}`);
|
|
132
|
+
}
|
|
70
133
|
// Check if path exists
|
|
71
134
|
try {
|
|
72
135
|
const stats = await fs.stat(absolute);
|
|
@@ -85,20 +148,23 @@ export async function validatePath(requestedPath) {
|
|
|
85
148
|
}
|
|
86
149
|
};
|
|
87
150
|
// Execute with timeout
|
|
88
|
-
const result = await withTimeout(validationOperation(), PATH_VALIDATION_TIMEOUT, `Path validation for
|
|
151
|
+
const result = await withTimeout(validationOperation(), PATH_VALIDATION_TIMEOUT, `Path validation operation`, // Generic name for telemetry
|
|
152
|
+
null);
|
|
89
153
|
if (result === null) {
|
|
90
|
-
//
|
|
91
|
-
|
|
154
|
+
// Keep original path in error for AI but a generic message for telemetry
|
|
155
|
+
capture('server_path_validation_timeout', {
|
|
156
|
+
timeoutMs: PATH_VALIDATION_TIMEOUT
|
|
157
|
+
});
|
|
158
|
+
throw new Error(`Path validation failed for path: ${requestedPath}`);
|
|
92
159
|
}
|
|
93
160
|
return result;
|
|
94
161
|
}
|
|
95
162
|
/**
|
|
96
163
|
* Read file content from a URL
|
|
97
164
|
* @param url URL to fetch content from
|
|
98
|
-
* @param returnMetadata Whether to return metadata with the content
|
|
99
165
|
* @returns File content or file result with metadata
|
|
100
166
|
*/
|
|
101
|
-
export async function readFileFromUrl(url
|
|
167
|
+
export async function readFileFromUrl(url) {
|
|
102
168
|
// Import the MIME type utilities
|
|
103
169
|
const { isImageFile } = await import('./mime-types.js');
|
|
104
170
|
// Set up fetch with timeout
|
|
@@ -121,22 +187,12 @@ export async function readFileFromUrl(url, returnMetadata) {
|
|
|
121
187
|
// For images, convert to base64
|
|
122
188
|
const buffer = await response.arrayBuffer();
|
|
123
189
|
const content = Buffer.from(buffer).toString('base64');
|
|
124
|
-
|
|
125
|
-
return { content, mimeType: contentType, isImage };
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
return content;
|
|
129
|
-
}
|
|
190
|
+
return { content, mimeType: contentType, isImage };
|
|
130
191
|
}
|
|
131
192
|
else {
|
|
132
193
|
// For text content
|
|
133
194
|
const content = await response.text();
|
|
134
|
-
|
|
135
|
-
return { content, mimeType: contentType, isImage };
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
return content;
|
|
139
|
-
}
|
|
195
|
+
return { content, mimeType: contentType, isImage };
|
|
140
196
|
}
|
|
141
197
|
}
|
|
142
198
|
catch (error) {
|
|
@@ -146,16 +202,7 @@ export async function readFileFromUrl(url, returnMetadata) {
|
|
|
146
202
|
const errorMessage = error instanceof DOMException && error.name === 'AbortError'
|
|
147
203
|
? `URL fetch timed out after ${FETCH_TIMEOUT_MS}ms: ${url}`
|
|
148
204
|
: `Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`;
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
content: `Error: ${errorMessage}`,
|
|
152
|
-
mimeType: 'text/plain',
|
|
153
|
-
isImage: false
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
return `Error: ${errorMessage}`;
|
|
158
|
-
}
|
|
205
|
+
throw new Error(errorMessage);
|
|
159
206
|
}
|
|
160
207
|
}
|
|
161
208
|
/**
|
|
@@ -164,31 +211,35 @@ export async function readFileFromUrl(url, returnMetadata) {
|
|
|
164
211
|
* @param returnMetadata Whether to return metadata with the content
|
|
165
212
|
* @returns File content or file result with metadata
|
|
166
213
|
*/
|
|
167
|
-
export async function readFileFromDisk(filePath
|
|
214
|
+
export async function readFileFromDisk(filePath) {
|
|
168
215
|
// Import the MIME type utilities
|
|
169
216
|
const { getMimeType, isImageFile } = await import('./mime-types.js');
|
|
170
217
|
const validPath = await validatePath(filePath);
|
|
218
|
+
// Get file extension for telemetry using path module consistently
|
|
219
|
+
const fileExtension = path.extname(validPath).toLowerCase();
|
|
171
220
|
// Check file size before attempting to read
|
|
172
221
|
try {
|
|
173
222
|
const stats = await fs.stat(validPath);
|
|
174
223
|
const MAX_SIZE = 100 * 1024; // 100KB limit
|
|
175
224
|
if (stats.size > MAX_SIZE) {
|
|
176
225
|
const message = `File too large (${(stats.size / 1024).toFixed(2)}KB > ${MAX_SIZE / 1024}KB limit)`;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
return message;
|
|
186
|
-
}
|
|
226
|
+
// Capture file extension in telemetry without capturing the file path
|
|
227
|
+
capture('server_read_file_large', { fileExtension: fileExtension });
|
|
228
|
+
return {
|
|
229
|
+
content: message,
|
|
230
|
+
mimeType: 'text/plain',
|
|
231
|
+
isImage: false
|
|
232
|
+
};
|
|
187
233
|
}
|
|
234
|
+
// Capture file extension in telemetry without capturing the file path
|
|
235
|
+
capture('server_read_file', { fileExtension: fileExtension });
|
|
188
236
|
}
|
|
189
237
|
catch (error) {
|
|
238
|
+
console.error('error catch ' + error);
|
|
239
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
240
|
+
capture('server_read_file_error', { error: errorMessage, fileExtension: fileExtension });
|
|
190
241
|
// If we can't stat the file, continue anyway and let the read operation handle errors
|
|
191
|
-
console.error(`Failed to stat file ${validPath}:`, error);
|
|
242
|
+
//console.error(`Failed to stat file ${validPath}:`, error);
|
|
192
243
|
}
|
|
193
244
|
// Detect the MIME type based on file extension
|
|
194
245
|
const mimeType = getMimeType(validPath);
|
|
@@ -200,41 +251,28 @@ export async function readFileFromDisk(filePath, returnMetadata) {
|
|
|
200
251
|
// For image files, read as Buffer and convert to base64
|
|
201
252
|
const buffer = await fs.readFile(validPath);
|
|
202
253
|
const content = buffer.toString('base64');
|
|
203
|
-
|
|
204
|
-
return { content, mimeType, isImage };
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
return content;
|
|
208
|
-
}
|
|
254
|
+
return { content, mimeType, isImage };
|
|
209
255
|
}
|
|
210
256
|
else {
|
|
211
257
|
// For all other files, try to read as UTF-8 text
|
|
212
258
|
try {
|
|
213
259
|
const content = await fs.readFile(validPath, "utf-8");
|
|
214
|
-
|
|
215
|
-
return { content, mimeType, isImage };
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
return content;
|
|
219
|
-
}
|
|
260
|
+
return { content, mimeType, isImage };
|
|
220
261
|
}
|
|
221
262
|
catch (error) {
|
|
222
263
|
// If UTF-8 reading fails, treat as binary and return base64 but still as text
|
|
223
264
|
const buffer = await fs.readFile(validPath);
|
|
224
265
|
const content = `Binary file content (base64 encoded):\n${buffer.toString('base64')}`;
|
|
225
|
-
|
|
226
|
-
return { content, mimeType: 'text/plain', isImage: false };
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
return content;
|
|
230
|
-
}
|
|
266
|
+
return { content, mimeType: 'text/plain', isImage: false };
|
|
231
267
|
}
|
|
232
268
|
}
|
|
233
269
|
};
|
|
234
270
|
// Execute with timeout
|
|
235
|
-
const result = await withTimeout(readOperation(), FILE_READ_TIMEOUT, `Read file operation for ${filePath}`,
|
|
236
|
-
|
|
237
|
-
|
|
271
|
+
const result = await withTimeout(readOperation(), FILE_READ_TIMEOUT, `Read file operation for ${filePath}`, null);
|
|
272
|
+
if (result == null) {
|
|
273
|
+
// Handles the impossible case where withTimeout resolves to null instead of throwing
|
|
274
|
+
throw new Error('Failed to read the file');
|
|
275
|
+
}
|
|
238
276
|
return result;
|
|
239
277
|
}
|
|
240
278
|
/**
|
|
@@ -244,20 +282,24 @@ export async function readFileFromDisk(filePath, returnMetadata) {
|
|
|
244
282
|
* @param isUrl Whether the path is a URL
|
|
245
283
|
* @returns File content or file result with metadata
|
|
246
284
|
*/
|
|
247
|
-
export async function readFile(filePath,
|
|
285
|
+
export async function readFile(filePath, isUrl) {
|
|
248
286
|
return isUrl
|
|
249
|
-
? readFileFromUrl(filePath
|
|
250
|
-
: readFileFromDisk(filePath
|
|
287
|
+
? readFileFromUrl(filePath)
|
|
288
|
+
: readFileFromDisk(filePath);
|
|
251
289
|
}
|
|
252
290
|
export async function writeFile(filePath, content) {
|
|
253
291
|
const validPath = await validatePath(filePath);
|
|
292
|
+
// Get file extension for telemetry
|
|
293
|
+
const fileExtension = path.extname(validPath).toLowerCase();
|
|
294
|
+
// Capture file extension in telemetry without capturing the file path
|
|
295
|
+
capture('server_write_file', { fileExtension: fileExtension });
|
|
254
296
|
await fs.writeFile(validPath, content, "utf-8");
|
|
255
297
|
}
|
|
256
298
|
export async function readMultipleFiles(paths) {
|
|
257
299
|
return Promise.all(paths.map(async (filePath) => {
|
|
258
300
|
try {
|
|
259
301
|
const validPath = await validatePath(filePath);
|
|
260
|
-
const fileResult = await readFile(validPath
|
|
302
|
+
const fileResult = await readFile(validPath);
|
|
261
303
|
return {
|
|
262
304
|
path: filePath,
|
|
263
305
|
content: typeof fileResult === 'string' ? fileResult : fileResult.content,
|
|
@@ -291,7 +333,19 @@ export async function moveFile(sourcePath, destinationPath) {
|
|
|
291
333
|
export async function searchFiles(rootPath, pattern) {
|
|
292
334
|
const results = [];
|
|
293
335
|
async function search(currentPath) {
|
|
294
|
-
|
|
336
|
+
let entries;
|
|
337
|
+
try {
|
|
338
|
+
entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
// Only sanitize for telemetry, not for the returned error
|
|
342
|
+
capture('server_search_files_error', {
|
|
343
|
+
errorType: error instanceof Error ? error.name : 'Unknown',
|
|
344
|
+
errorMessage: 'Error reading directory',
|
|
345
|
+
isReadDirError: true
|
|
346
|
+
});
|
|
347
|
+
return; // Skip this directory on error
|
|
348
|
+
}
|
|
295
349
|
for (const entry of entries) {
|
|
296
350
|
const fullPath = path.join(currentPath, entry.name);
|
|
297
351
|
try {
|
|
@@ -308,10 +362,27 @@ export async function searchFiles(rootPath, pattern) {
|
|
|
308
362
|
}
|
|
309
363
|
}
|
|
310
364
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
365
|
+
try {
|
|
366
|
+
// Validate root path before starting search
|
|
367
|
+
const validPath = await validatePath(rootPath);
|
|
368
|
+
await search(validPath);
|
|
369
|
+
// Log only the count of found files, not their paths
|
|
370
|
+
capture('server_search_files_complete', {
|
|
371
|
+
resultsCount: results.length,
|
|
372
|
+
patternLength: pattern.length
|
|
373
|
+
});
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
// For telemetry only - sanitize error info
|
|
378
|
+
capture('server_search_files_error', {
|
|
379
|
+
errorType: error instanceof Error ? error.name : 'Unknown',
|
|
380
|
+
errorMessage: 'Error with root path',
|
|
381
|
+
isRootPathError: true
|
|
382
|
+
});
|
|
383
|
+
// Re-throw the original error for the caller
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
315
386
|
}
|
|
316
387
|
export async function getFileInfo(filePath) {
|
|
317
388
|
const validPath = await validatePath(filePath);
|
|
@@ -326,6 +397,5 @@ export async function getFileInfo(filePath) {
|
|
|
326
397
|
permissions: stats.mode.toString(8).slice(-3),
|
|
327
398
|
};
|
|
328
399
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
400
|
+
// This function has been replaced with configManager.getConfig()
|
|
401
|
+
// Use get_config tool to retrieve allowedDirectories
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively finds the closest match to a query string within text using fuzzy matching
|
|
3
|
+
* @param text The text to search within
|
|
4
|
+
* @param query The query string to find
|
|
5
|
+
* @param start Start index in the text (default: 0)
|
|
6
|
+
* @param end End index in the text (default: text.length)
|
|
7
|
+
* @param parentDistance Best distance found so far (default: Infinity)
|
|
8
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
9
|
+
*/
|
|
10
|
+
export declare function recursiveFuzzyIndexOf(text: string, query: string, start?: number, end?: number | null, parentDistance?: number, depth?: number): {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
value: string;
|
|
14
|
+
distance: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Calculates the similarity ratio between two strings
|
|
18
|
+
* @param a First string
|
|
19
|
+
* @param b Second string
|
|
20
|
+
* @returns Similarity ratio (0-1)
|
|
21
|
+
*/
|
|
22
|
+
export declare function getSimilarityRatio(a: string, b: string): number;
|