@wonderwhy-er/desktop-commander 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -2
- package/dist/config-manager.d.ts +42 -0
- package/dist/config-manager.js +242 -0
- package/dist/server.js +40 -1
- package/dist/tools/config.d.ts +83 -0
- package/dist/tools/config.js +183 -0
- package/dist/tools/filesystem.js +31 -7
- package/dist/tools/schemas.d.ts +25 -0
- package/dist/tools/schemas.js +10 -0
- package/dist/tools/search.d.ts +32 -0
- package/dist/tools/search.js +164 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/logo.png +0 -0
- package/package.json +7 -3
- package/testemonials/img.png +0 -0
- package/testemonials/img_1.png +0 -0
- package/testemonials/img_2.png +0 -0
- package/testemonials/img_3.png +0 -0
- package/testemonials/img_4.png +0 -0
package/README.md
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Desktop Commander MCP
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
[](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
|
|
4
5
|
[](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
|
|
6
|
+
[](https://www.buymeacoffee.com/wonderwhyer)
|
|
5
7
|
|
|
8
|
+
[](https://discord.gg/kQ27sNnZr7)
|
|
6
9
|
|
|
7
10
|
Short version. Two key things. Terminal commands and diff based file editing.
|
|
8
11
|
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
<a href="https://glama.ai/mcp/servers/zempur9oh4">
|
|
15
|
+
<img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Claude Desktop Commander MCP server" />
|
|
16
|
+
</a>
|
|
17
|
+
|
|
18
|
+
## Table of Contents
|
|
19
|
+
- [Features](#features)
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
- [Usage](#usage)
|
|
22
|
+
- [Handling Long-Running Commands](#handling-long-running-commands)
|
|
23
|
+
- [Work in Progress and TODOs](#work-in-progress-and-todos)
|
|
24
|
+
- [Media links](#media)
|
|
25
|
+
- [Testimonials](#testimonials)
|
|
26
|
+
- [Contributing](#contributing)
|
|
27
|
+
- [License](#license)
|
|
28
|
+
|
|
9
29
|
This is server that allows Claude desktop app to execute long-running terminal commands on your computer and manage processes through Model Context Protocol (MCP) + Built on top of [MCP Filesystem Server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) to provide additional search and replace file editing capabilities .
|
|
10
30
|
|
|
11
31
|
## Features
|
|
@@ -25,6 +45,7 @@ This is server that allows Claude desktop app to execute long-running terminal c
|
|
|
25
45
|
- Full file rewrites for major changes
|
|
26
46
|
- Multiple file support
|
|
27
47
|
- Pattern-based replacements
|
|
48
|
+
- vscode-ripgrep based recursive code or text search in folders
|
|
28
49
|
|
|
29
50
|
## Installation
|
|
30
51
|
First, ensure you've downloaded and installed the [Claude Desktop app](https://claude.ai/download) and you have [npm installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
|
|
@@ -95,6 +116,7 @@ The server provides these tool categories:
|
|
|
95
116
|
- `move_file`: Move/rename files
|
|
96
117
|
- `search_files`: Pattern-based file search
|
|
97
118
|
- `get_file_info`: File metadata
|
|
119
|
+
- `code_search`: Recursive ripgrep based text and code search
|
|
98
120
|
|
|
99
121
|
### Edit Tools
|
|
100
122
|
- `edit_block`: Apply surgical text replacements (best for changes <20% of file size)
|
|
@@ -140,6 +162,55 @@ This project extends the MCP Filesystem Server to enable:
|
|
|
140
162
|
|
|
141
163
|
Created as part of exploring Claude MCPs: https://youtube.com/live/TlbjFDbl5Us
|
|
142
164
|
|
|
165
|
+
## DONE
|
|
166
|
+
- **25-03-2025 Better code search** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/17)) - Enhanced code exploration with context-aware results
|
|
167
|
+
|
|
168
|
+
## Work in Progress and TODOs
|
|
169
|
+
|
|
170
|
+
The following features are currently being developed or planned:
|
|
171
|
+
|
|
172
|
+
- **Better configurations** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/16)) - Improved settings for allowed paths, commands and shell environment
|
|
173
|
+
- **Windows environment fixes** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/13)) - Resolving issues specific to Windows platforms
|
|
174
|
+
- **Linux improvements** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/12)) - Enhancing compatibility with various Linux distributions
|
|
175
|
+
- **Support for WSL** - Windows Subsystem for Linux integration
|
|
176
|
+
- **Support for SSH** - Remote server command execution
|
|
177
|
+
- **Installation troubleshooting guide** - Comprehensive help for setup issues
|
|
178
|
+
|
|
179
|
+
## Media
|
|
180
|
+
Learn more about this project through these resources:
|
|
181
|
+
|
|
182
|
+
### Article
|
|
183
|
+
[Claude with MCPs replaced Cursor & Windsurf. How did that happen?](https://wonderwhy-er.medium.com/claude-with-mcps-replaced-cursor-windsurf-how-did-that-happen-c1d1e2795e96) - A detailed exploration of how Claude with Model Context Protocol capabilities is changing developer workflows.
|
|
184
|
+
|
|
185
|
+
### Video
|
|
186
|
+
[Claude Desktop Commander Video Tutorial](https://www.youtube.com/watch?v=ly3bed99Dy8) - Watch how to set up and use the Commander effectively.
|
|
187
|
+
|
|
188
|
+
### Community
|
|
189
|
+
Join our [Discord server](https://discord.gg/7cbccwRp) to get help, share feedback, and connect with other users.
|
|
190
|
+
|
|
191
|
+
## Testimonials
|
|
192
|
+
|
|
193
|
+
[ https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyyBt6_ShdDX_rIOad4AaABAg
|
|
194
|
+
](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyyBt6_ShdDX_rIOad4AaABAg
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
[
|
|
198
|
+
https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
[
|
|
203
|
+
https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg)
|
|
204
|
+
|
|
205
|
+
[
|
|
206
|
+
https://www.youtube.com/watch?v=ly3bed99Dy8&lc=Ugy4-exy166_Ma7TH-h4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=Ugy4-exy166_Ma7TH-h4AaABAg)
|
|
207
|
+
|
|
208
|
+
[
|
|
212
|
+
https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e](https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e)
|
|
213
|
+
|
|
143
214
|
## Contributing
|
|
144
215
|
|
|
145
216
|
If you find this project useful, please consider giving it a ⭐ star on GitHub! This helps others discover the project and encourages further development.
|
|
@@ -153,6 +224,8 @@ We welcome contributions from the community! Whether you've found a bug, have a
|
|
|
153
224
|
|
|
154
225
|
All contributions, big or small, are greatly appreciated!
|
|
155
226
|
|
|
227
|
+
If you find this tool valuable for your workflow, please consider [supporting the project](https://www.buymeacoffee.com/wonderwhyer).
|
|
228
|
+
|
|
156
229
|
## License
|
|
157
230
|
|
|
158
|
-
MIT
|
|
231
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for the server configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface ServerConfig {
|
|
5
|
+
blockedCommands?: string[];
|
|
6
|
+
defaultShell?: string;
|
|
7
|
+
logLevel?: 'error' | 'warn' | 'info' | 'debug';
|
|
8
|
+
allowedDirectories?: string[];
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Manages reading and writing server configuration
|
|
13
|
+
*/
|
|
14
|
+
export declare class ConfigManager {
|
|
15
|
+
private config;
|
|
16
|
+
private initialized;
|
|
17
|
+
/**
|
|
18
|
+
* Load configuration from disk
|
|
19
|
+
*/
|
|
20
|
+
loadConfig(): Promise<ServerConfig>;
|
|
21
|
+
/**
|
|
22
|
+
* Save current configuration to disk
|
|
23
|
+
*/
|
|
24
|
+
saveConfig(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Get a specific configuration value
|
|
27
|
+
*/
|
|
28
|
+
getValue<T>(key: string): Promise<T | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Set a specific configuration value
|
|
31
|
+
*/
|
|
32
|
+
setValue<T>(key: string, value: T): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Get the entire configuration object
|
|
35
|
+
*/
|
|
36
|
+
getConfig(): Promise<ServerConfig>;
|
|
37
|
+
/**
|
|
38
|
+
* Update multiple configuration values at once
|
|
39
|
+
*/
|
|
40
|
+
updateConfig(partialConfig: Partial<ServerConfig>): Promise<ServerConfig>;
|
|
41
|
+
}
|
|
42
|
+
export declare const configManager: ConfigManager;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CONFIG_FILE } from './config.js';
|
|
4
|
+
import * as process from 'process';
|
|
5
|
+
/**
|
|
6
|
+
* Manages reading and writing server configuration
|
|
7
|
+
*/
|
|
8
|
+
export class ConfigManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.config = {};
|
|
11
|
+
this.initialized = false;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Load configuration from disk
|
|
15
|
+
*/
|
|
16
|
+
async loadConfig() {
|
|
17
|
+
try {
|
|
18
|
+
console.error(`Loading config from ${CONFIG_FILE}...`);
|
|
19
|
+
console.error(`Current working directory: ${process.cwd()}`);
|
|
20
|
+
console.error(`Absolute config path: ${path.resolve(CONFIG_FILE)}`);
|
|
21
|
+
// Ensure config directory exists
|
|
22
|
+
const configDir = path.dirname(CONFIG_FILE);
|
|
23
|
+
try {
|
|
24
|
+
console.error(`Ensuring config directory exists: ${configDir}`);
|
|
25
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
26
|
+
console.error(`Config directory ready: ${configDir}`);
|
|
27
|
+
}
|
|
28
|
+
catch (mkdirError) {
|
|
29
|
+
console.error(`Error creating config directory: ${mkdirError.message}`);
|
|
30
|
+
// Continue if directory already exists
|
|
31
|
+
if (mkdirError.code !== 'EEXIST') {
|
|
32
|
+
throw mkdirError;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Check if the directory exists and is writable
|
|
36
|
+
try {
|
|
37
|
+
const dirStats = await fs.stat(configDir);
|
|
38
|
+
console.error(`Config directory exists: ${dirStats.isDirectory()}`);
|
|
39
|
+
await fs.access(configDir, fs.constants.W_OK);
|
|
40
|
+
console.error(`Directory ${configDir} is writable`);
|
|
41
|
+
}
|
|
42
|
+
catch (dirError) {
|
|
43
|
+
console.error(`Config directory check error: ${dirError.message}`);
|
|
44
|
+
}
|
|
45
|
+
// Check file permissions
|
|
46
|
+
try {
|
|
47
|
+
const fileStats = await fs.stat(CONFIG_FILE).catch(() => null);
|
|
48
|
+
if (fileStats) {
|
|
49
|
+
console.error(`Config file exists, permissions: ${fileStats.mode.toString(8)}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.error('Config file does not exist, will create');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (statError) {
|
|
56
|
+
console.error(`Error checking file stats: ${statError.message}`);
|
|
57
|
+
}
|
|
58
|
+
let configData;
|
|
59
|
+
try {
|
|
60
|
+
configData = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
61
|
+
console.error(`Config file read successfully, content length: ${configData.length}`);
|
|
62
|
+
}
|
|
63
|
+
catch (readError) {
|
|
64
|
+
console.error(`Error reading config file: ${readError.message}, code: ${readError.code}, stack: ${readError.stack}`);
|
|
65
|
+
if (readError.code === 'ENOENT') {
|
|
66
|
+
console.error('Config file does not exist, will create default');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
throw readError;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (configData) {
|
|
73
|
+
try {
|
|
74
|
+
this.config = JSON.parse(configData);
|
|
75
|
+
console.error(`Config parsed successfully: ${JSON.stringify(this.config, null, 2)}`);
|
|
76
|
+
}
|
|
77
|
+
catch (parseError) {
|
|
78
|
+
console.error(`Failed to parse config JSON: ${parseError.message}`);
|
|
79
|
+
// If file exists but has invalid JSON, use default empty config
|
|
80
|
+
this.config = {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// If file doesn't exist, use default empty config
|
|
85
|
+
this.config = {};
|
|
86
|
+
}
|
|
87
|
+
this.initialized = true;
|
|
88
|
+
// Create default config file if it doesn't exist
|
|
89
|
+
if (!configData) {
|
|
90
|
+
console.error('Creating default config file');
|
|
91
|
+
await this.saveConfig();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error(`Unexpected error in loadConfig: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
97
|
+
// Initialize with empty config
|
|
98
|
+
this.config = {};
|
|
99
|
+
this.initialized = true; // Mark as initialized even with empty config
|
|
100
|
+
}
|
|
101
|
+
return this.config;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Save current configuration to disk
|
|
105
|
+
*/
|
|
106
|
+
async saveConfig() {
|
|
107
|
+
try {
|
|
108
|
+
console.error(`Saving config to ${CONFIG_FILE}...`);
|
|
109
|
+
console.error(`Current working directory: ${process.cwd()}`);
|
|
110
|
+
console.error(`Absolute config path: ${path.resolve(CONFIG_FILE)}`);
|
|
111
|
+
// Always try to create the config directory first
|
|
112
|
+
const configDir = path.dirname(CONFIG_FILE);
|
|
113
|
+
try {
|
|
114
|
+
console.error(`Ensuring config directory exists: ${configDir}`);
|
|
115
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
116
|
+
console.error(`Config directory ready: ${configDir}`);
|
|
117
|
+
}
|
|
118
|
+
catch (mkdirError) {
|
|
119
|
+
console.error(`Failed to create directory: ${mkdirError.message}`);
|
|
120
|
+
if (mkdirError.code !== 'EEXIST') {
|
|
121
|
+
throw mkdirError;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Check directory permissions
|
|
125
|
+
try {
|
|
126
|
+
await fs.access(configDir, fs.constants.W_OK);
|
|
127
|
+
console.error(`Directory ${configDir} is writable`);
|
|
128
|
+
}
|
|
129
|
+
catch (accessError) {
|
|
130
|
+
console.error(`Directory access error: ${accessError.message}`);
|
|
131
|
+
throw new Error(`Config directory is not writable: ${accessError.message}`);
|
|
132
|
+
}
|
|
133
|
+
const configJson = JSON.stringify(this.config, null, 2);
|
|
134
|
+
console.error(`Config to save: ${configJson}`);
|
|
135
|
+
try {
|
|
136
|
+
// Try to write the file with explicit encoding and permissions
|
|
137
|
+
await fs.writeFile(CONFIG_FILE, configJson, {
|
|
138
|
+
encoding: 'utf-8',
|
|
139
|
+
mode: 0o644 // Readable/writable by owner, readable by others
|
|
140
|
+
});
|
|
141
|
+
console.error('Config saved successfully');
|
|
142
|
+
}
|
|
143
|
+
catch (writeError) {
|
|
144
|
+
console.error(`Write file error: ${writeError.message}, code: ${writeError.code}, stack: ${writeError.stack}`);
|
|
145
|
+
throw writeError;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.error(`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
151
|
+
throw new Error(`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get a specific configuration value
|
|
156
|
+
*/
|
|
157
|
+
async getValue(key) {
|
|
158
|
+
if (!this.initialized) {
|
|
159
|
+
console.error(`getValue for key "${key}" - loading config first`);
|
|
160
|
+
await this.loadConfig();
|
|
161
|
+
}
|
|
162
|
+
console.error(`Getting value for key "${key}": ${JSON.stringify(this.config[key])}`);
|
|
163
|
+
return this.config[key];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Set a specific configuration value
|
|
167
|
+
*/
|
|
168
|
+
async setValue(key, value) {
|
|
169
|
+
console.error(`Setting value for key "${key}": ${JSON.stringify(value)}`);
|
|
170
|
+
if (!this.initialized) {
|
|
171
|
+
console.error('setValue - loading config first');
|
|
172
|
+
await this.loadConfig();
|
|
173
|
+
}
|
|
174
|
+
this.config[key] = value;
|
|
175
|
+
await this.saveConfig();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get the entire configuration object
|
|
179
|
+
*/
|
|
180
|
+
async getConfig() {
|
|
181
|
+
if (!this.initialized) {
|
|
182
|
+
console.error('getConfig - loading config first');
|
|
183
|
+
await this.loadConfig();
|
|
184
|
+
}
|
|
185
|
+
console.error(`Getting full config: ${JSON.stringify(this.config, null, 2)}`);
|
|
186
|
+
return { ...this.config }; // Return a copy to prevent untracked mutations
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Update multiple configuration values at once
|
|
190
|
+
*/
|
|
191
|
+
async updateConfig(partialConfig) {
|
|
192
|
+
console.error(`Updating config with: ${JSON.stringify(partialConfig, null, 2)}`);
|
|
193
|
+
if (!this.initialized) {
|
|
194
|
+
console.error('updateConfig - loading config first');
|
|
195
|
+
await this.loadConfig();
|
|
196
|
+
}
|
|
197
|
+
this.config = {
|
|
198
|
+
...this.config,
|
|
199
|
+
...partialConfig
|
|
200
|
+
};
|
|
201
|
+
await this.saveConfig();
|
|
202
|
+
return { ...this.config };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Memory-only version that doesn't try to save to filesystem
|
|
206
|
+
class MemoryConfigManager {
|
|
207
|
+
constructor() {
|
|
208
|
+
this.config = {};
|
|
209
|
+
this.initialized = true;
|
|
210
|
+
}
|
|
211
|
+
async loadConfig() {
|
|
212
|
+
console.error('Using memory-only configuration (no filesystem operations)');
|
|
213
|
+
return this.config;
|
|
214
|
+
}
|
|
215
|
+
async saveConfig() {
|
|
216
|
+
console.error('Memory-only configuration - changes will not persist after restart');
|
|
217
|
+
// No-op - we don't save to filesystem
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
async getValue(key) {
|
|
221
|
+
console.error(`Getting memory value for key "${key}": ${JSON.stringify(this.config[key])}`);
|
|
222
|
+
return this.config[key];
|
|
223
|
+
}
|
|
224
|
+
async setValue(key, value) {
|
|
225
|
+
console.error(`Setting memory value for key "${key}": ${JSON.stringify(value)}`);
|
|
226
|
+
this.config[key] = value;
|
|
227
|
+
}
|
|
228
|
+
async getConfig() {
|
|
229
|
+
console.error(`Getting full memory config: ${JSON.stringify(this.config, null, 2)}`);
|
|
230
|
+
return { ...this.config };
|
|
231
|
+
}
|
|
232
|
+
async updateConfig(partialConfig) {
|
|
233
|
+
console.error(`Updating memory config with: ${JSON.stringify(partialConfig, null, 2)}`);
|
|
234
|
+
this.config = {
|
|
235
|
+
...this.config,
|
|
236
|
+
...partialConfig
|
|
237
|
+
};
|
|
238
|
+
return { ...this.config };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Export the appropriate manager based on the environment
|
|
242
|
+
export const configManager = new ConfigManager();
|
package/dist/server.js
CHANGED
|
@@ -2,11 +2,12 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
2
2
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
4
|
import { commandManager } from './command-manager.js';
|
|
5
|
-
import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, BlockCommandArgsSchema, UnblockCommandArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, SearchFilesArgsSchema, GetFileInfoArgsSchema, EditBlockArgsSchema, } from './tools/schemas.js';
|
|
5
|
+
import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, BlockCommandArgsSchema, UnblockCommandArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, SearchFilesArgsSchema, GetFileInfoArgsSchema, EditBlockArgsSchema, SearchCodeArgsSchema, } from './tools/schemas.js';
|
|
6
6
|
import { executeCommand, readOutput, forceTerminate, listSessions } from './tools/execute.js';
|
|
7
7
|
import { listProcesses, killProcess } from './tools/process.js';
|
|
8
8
|
import { readFile, readMultipleFiles, writeFile, createDirectory, listDirectory, moveFile, searchFiles, getFileInfo, listAllowedDirectories, } from './tools/filesystem.js';
|
|
9
9
|
import { parseEditBlock, performSearchReplace } from './tools/edit.js';
|
|
10
|
+
import { searchTextInFiles } from './tools/search.js';
|
|
10
11
|
import { VERSION } from './version.js';
|
|
11
12
|
export const server = new Server({
|
|
12
13
|
name: "desktop-commander",
|
|
@@ -124,6 +125,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
124
125
|
"Only searches within allowed directories.",
|
|
125
126
|
inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
|
|
126
127
|
},
|
|
128
|
+
{
|
|
129
|
+
name: "search_code",
|
|
130
|
+
description: "Search for text/code patterns within file contents using ripgrep. " +
|
|
131
|
+
"Fast and powerful search similar to VS Code search functionality. " +
|
|
132
|
+
"Supports regular expressions, file pattern filtering, and context lines. " +
|
|
133
|
+
"Only searches within allowed directories.",
|
|
134
|
+
inputSchema: zodToJsonSchema(SearchCodeArgsSchema),
|
|
135
|
+
},
|
|
127
136
|
{
|
|
128
137
|
name: "get_file_info",
|
|
129
138
|
description: "Retrieve detailed metadata about a file or directory including size, " +
|
|
@@ -253,6 +262,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
253
262
|
content: [{ type: "text", text: results.length > 0 ? results.join('\n') : "No matches found" }],
|
|
254
263
|
};
|
|
255
264
|
}
|
|
265
|
+
case "search_code": {
|
|
266
|
+
const parsed = SearchCodeArgsSchema.parse(args);
|
|
267
|
+
const results = await searchTextInFiles({
|
|
268
|
+
rootPath: parsed.path,
|
|
269
|
+
pattern: parsed.pattern,
|
|
270
|
+
filePattern: parsed.filePattern,
|
|
271
|
+
ignoreCase: parsed.ignoreCase,
|
|
272
|
+
maxResults: parsed.maxResults,
|
|
273
|
+
includeHidden: parsed.includeHidden,
|
|
274
|
+
contextLines: parsed.contextLines,
|
|
275
|
+
});
|
|
276
|
+
if (results.length === 0) {
|
|
277
|
+
return {
|
|
278
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Format the results in a VS Code-like format
|
|
282
|
+
let currentFile = "";
|
|
283
|
+
let formattedResults = "";
|
|
284
|
+
results.forEach(result => {
|
|
285
|
+
if (result.file !== currentFile) {
|
|
286
|
+
formattedResults += `\n${result.file}:\n`;
|
|
287
|
+
currentFile = result.file;
|
|
288
|
+
}
|
|
289
|
+
formattedResults += ` ${result.line}: ${result.match}\n`;
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text", text: formattedResults.trim() }],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
256
295
|
case "get_file_info": {
|
|
257
296
|
const parsed = GetFileInfoArgsSchema.parse(args);
|
|
258
297
|
const info = await getFileInfo(parsed.path);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const GetConfigArgsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
3
|
+
export declare const GetConfigValueArgsSchema: z.ZodObject<{
|
|
4
|
+
key: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
key: string;
|
|
7
|
+
}, {
|
|
8
|
+
key: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const SetConfigValueArgsSchema: z.ZodObject<{
|
|
11
|
+
key: z.ZodString;
|
|
12
|
+
value: z.ZodAny;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
key: string;
|
|
15
|
+
value?: any;
|
|
16
|
+
}, {
|
|
17
|
+
key: string;
|
|
18
|
+
value?: any;
|
|
19
|
+
}>;
|
|
20
|
+
export declare const UpdateConfigArgsSchema: z.ZodObject<{
|
|
21
|
+
config: z.ZodRecord<z.ZodString, z.ZodAny>;
|
|
22
|
+
}, "strip", z.ZodTypeAny, {
|
|
23
|
+
config: Record<string, any>;
|
|
24
|
+
}, {
|
|
25
|
+
config: Record<string, any>;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the entire config
|
|
29
|
+
*/
|
|
30
|
+
export declare function getConfig(): Promise<{
|
|
31
|
+
content: {
|
|
32
|
+
type: string;
|
|
33
|
+
text: string;
|
|
34
|
+
}[];
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Get a specific config value
|
|
38
|
+
*/
|
|
39
|
+
export declare function getConfigValue(args: unknown): Promise<{
|
|
40
|
+
content: {
|
|
41
|
+
type: string;
|
|
42
|
+
text: string;
|
|
43
|
+
}[];
|
|
44
|
+
isError: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
content: {
|
|
47
|
+
type: string;
|
|
48
|
+
text: string;
|
|
49
|
+
}[];
|
|
50
|
+
isError?: undefined;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Set a specific config value
|
|
54
|
+
*/
|
|
55
|
+
export declare function setConfigValue(args: unknown): Promise<{
|
|
56
|
+
content: {
|
|
57
|
+
type: string;
|
|
58
|
+
text: string;
|
|
59
|
+
}[];
|
|
60
|
+
isError: boolean;
|
|
61
|
+
} | {
|
|
62
|
+
content: {
|
|
63
|
+
type: string;
|
|
64
|
+
text: string;
|
|
65
|
+
}[];
|
|
66
|
+
isError?: undefined;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Update multiple config values at once
|
|
70
|
+
*/
|
|
71
|
+
export declare function updateConfig(args: unknown): Promise<{
|
|
72
|
+
content: {
|
|
73
|
+
type: string;
|
|
74
|
+
text: string;
|
|
75
|
+
}[];
|
|
76
|
+
isError: boolean;
|
|
77
|
+
} | {
|
|
78
|
+
content: {
|
|
79
|
+
type: string;
|
|
80
|
+
text: string;
|
|
81
|
+
}[];
|
|
82
|
+
isError?: undefined;
|
|
83
|
+
}>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { configManager } from '../config-manager.js';
|
|
3
|
+
// Schemas for config operations
|
|
4
|
+
export const GetConfigArgsSchema = z.object({});
|
|
5
|
+
export const GetConfigValueArgsSchema = z.object({
|
|
6
|
+
key: z.string(),
|
|
7
|
+
});
|
|
8
|
+
export const SetConfigValueArgsSchema = z.object({
|
|
9
|
+
key: z.string(),
|
|
10
|
+
value: z.any(),
|
|
11
|
+
});
|
|
12
|
+
export const UpdateConfigArgsSchema = z.object({
|
|
13
|
+
config: z.record(z.any()),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Get the entire config
|
|
17
|
+
*/
|
|
18
|
+
export async function getConfig() {
|
|
19
|
+
console.error('getConfig called');
|
|
20
|
+
try {
|
|
21
|
+
const config = await configManager.getConfig();
|
|
22
|
+
console.error(`getConfig result: ${JSON.stringify(config, null, 2)}`);
|
|
23
|
+
return {
|
|
24
|
+
content: [{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: `Current configuration:\n${JSON.stringify(config, null, 2)}`
|
|
27
|
+
}],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(`Error in getConfig: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
33
|
+
// Return empty config rather than crashing
|
|
34
|
+
return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: `Error getting configuration: ${error instanceof Error ? error.message : String(error)}\nUsing empty configuration.`
|
|
38
|
+
}],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get a specific config value
|
|
44
|
+
*/
|
|
45
|
+
export async function getConfigValue(args) {
|
|
46
|
+
console.error(`getConfigValue called with args: ${JSON.stringify(args)}`);
|
|
47
|
+
try {
|
|
48
|
+
const parsed = GetConfigValueArgsSchema.safeParse(args);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
console.error(`Invalid arguments for get_config_value: ${parsed.error}`);
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `Invalid arguments: ${parsed.error}`
|
|
55
|
+
}],
|
|
56
|
+
isError: true
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const value = await configManager.getValue(parsed.data.key);
|
|
60
|
+
console.error(`getConfigValue result for key ${parsed.data.key}: ${JSON.stringify(value)}`);
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: value !== undefined
|
|
65
|
+
? `Value for ${parsed.data.key}: ${JSON.stringify(value, null, 2)}`
|
|
66
|
+
: `No value found for key: ${parsed.data.key}`
|
|
67
|
+
}],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(`Error in getConfigValue: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: `Error retrieving value: ${error instanceof Error ? error.message : String(error)}`
|
|
77
|
+
}],
|
|
78
|
+
isError: true
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Set a specific config value
|
|
84
|
+
*/
|
|
85
|
+
export async function setConfigValue(args) {
|
|
86
|
+
console.error(`setConfigValue called with args: ${JSON.stringify(args)}`);
|
|
87
|
+
try {
|
|
88
|
+
const parsed = SetConfigValueArgsSchema.safeParse(args);
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
console.error(`Invalid arguments for set_config_value: ${parsed.error}`);
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: "text",
|
|
94
|
+
text: `Invalid arguments: ${parsed.error}`
|
|
95
|
+
}],
|
|
96
|
+
isError: true
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await configManager.setValue(parsed.data.key, parsed.data.value);
|
|
101
|
+
console.error(`setConfigValue: Successfully set ${parsed.data.key} to ${JSON.stringify(parsed.data.value)}`);
|
|
102
|
+
return {
|
|
103
|
+
content: [{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `Successfully set ${parsed.data.key} to ${JSON.stringify(parsed.data.value, null, 2)}`
|
|
106
|
+
}],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (saveError) {
|
|
110
|
+
console.error(`Error saving config: ${saveError.message}`);
|
|
111
|
+
// Continue with in-memory change but report error
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: `Value changed in memory but couldn't be saved to disk: ${saveError.message}`
|
|
116
|
+
}],
|
|
117
|
+
isError: true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error(`Error in setConfigValue: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `Error setting value: ${error instanceof Error ? error.message : String(error)}`
|
|
128
|
+
}],
|
|
129
|
+
isError: true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Update multiple config values at once
|
|
135
|
+
*/
|
|
136
|
+
export async function updateConfig(args) {
|
|
137
|
+
console.error(`updateConfig called with args: ${JSON.stringify(args)}`);
|
|
138
|
+
try {
|
|
139
|
+
const parsed = UpdateConfigArgsSchema.safeParse(args);
|
|
140
|
+
if (!parsed.success) {
|
|
141
|
+
console.error(`Invalid arguments for update_config: ${parsed.error}`);
|
|
142
|
+
return {
|
|
143
|
+
content: [{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: `Invalid arguments: ${parsed.error}`
|
|
146
|
+
}],
|
|
147
|
+
isError: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const updatedConfig = await configManager.updateConfig(parsed.data.config);
|
|
152
|
+
console.error(`updateConfig result: ${JSON.stringify(updatedConfig, null, 2)}`);
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `Configuration updated successfully.\nNew configuration:\n${JSON.stringify(updatedConfig, null, 2)}`
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (saveError) {
|
|
161
|
+
console.error(`Error saving updated config: ${saveError.message}`);
|
|
162
|
+
// Return useful response instead of crashing
|
|
163
|
+
return {
|
|
164
|
+
content: [{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `Configuration updated in memory but couldn't be saved to disk: ${saveError.message}`
|
|
167
|
+
}],
|
|
168
|
+
isError: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error(`Error in updateConfig: ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
console.error(error instanceof Error && error.stack ? error.stack : 'No stack trace available');
|
|
175
|
+
return {
|
|
176
|
+
content: [{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: `Error updating configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
179
|
+
}],
|
|
180
|
+
isError: true
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from 'os';
|
|
4
|
-
// Store allowed directories
|
|
4
|
+
// Store allowed directories - temporarily allowing all paths
|
|
5
|
+
// TODO: Make this configurable through a configuration file
|
|
5
6
|
const allowedDirectories = [
|
|
7
|
+
"/" // Root directory - effectively allows all paths
|
|
8
|
+
];
|
|
9
|
+
// Original implementation commented out for future reference
|
|
10
|
+
/*
|
|
11
|
+
const allowedDirectories: string[] = [
|
|
6
12
|
process.cwd(), // Current working directory
|
|
7
|
-
os.homedir()
|
|
13
|
+
os.homedir() // User's home directory
|
|
8
14
|
];
|
|
15
|
+
*/
|
|
9
16
|
// Normalize all paths consistently
|
|
10
17
|
function normalizePath(p) {
|
|
11
18
|
return path.normalize(p).toLowerCase();
|
|
@@ -18,16 +25,34 @@ function expandHome(filepath) {
|
|
|
18
25
|
}
|
|
19
26
|
// Security utilities
|
|
20
27
|
export async function validatePath(requestedPath) {
|
|
28
|
+
// Temporarily allow all paths by just returning the resolved path
|
|
29
|
+
// TODO: Implement configurable path validation
|
|
30
|
+
const expandedPath = expandHome(requestedPath);
|
|
31
|
+
const absolute = path.isAbsolute(expandedPath)
|
|
32
|
+
? path.resolve(expandedPath)
|
|
33
|
+
: path.resolve(process.cwd(), expandedPath);
|
|
34
|
+
// Try to resolve real path for symlinks, but don't enforce restrictions
|
|
35
|
+
try {
|
|
36
|
+
return await fs.realpath(absolute);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// If can't resolve (e.g., file doesn't exist yet), return absolute path
|
|
40
|
+
return absolute;
|
|
41
|
+
}
|
|
42
|
+
/* Original implementation commented out for future reference
|
|
21
43
|
const expandedPath = expandHome(requestedPath);
|
|
22
44
|
const absolute = path.isAbsolute(expandedPath)
|
|
23
45
|
? path.resolve(expandedPath)
|
|
24
46
|
: path.resolve(process.cwd(), expandedPath);
|
|
47
|
+
|
|
25
48
|
const normalizedRequested = normalizePath(absolute);
|
|
49
|
+
|
|
26
50
|
// Check if path is within allowed directories
|
|
27
51
|
const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(normalizePath(dir)));
|
|
28
52
|
if (!isAllowed) {
|
|
29
53
|
throw new Error(`Access denied - path outside allowed directories: ${absolute}`);
|
|
30
54
|
}
|
|
55
|
+
|
|
31
56
|
// Handle symlinks by checking their real path
|
|
32
57
|
try {
|
|
33
58
|
const realPath = await fs.realpath(absolute);
|
|
@@ -37,8 +62,7 @@ export async function validatePath(requestedPath) {
|
|
|
37
62
|
throw new Error("Access denied - symlink target outside allowed directories");
|
|
38
63
|
}
|
|
39
64
|
return realPath;
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
65
|
+
} catch (error) {
|
|
42
66
|
// For new files that don't exist yet, verify parent directory
|
|
43
67
|
const parentDir = path.dirname(absolute);
|
|
44
68
|
try {
|
|
@@ -49,11 +73,11 @@ export async function validatePath(requestedPath) {
|
|
|
49
73
|
throw new Error("Access denied - parent directory outside allowed directories");
|
|
50
74
|
}
|
|
51
75
|
return absolute;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
76
|
+
} catch {
|
|
54
77
|
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
55
78
|
}
|
|
56
79
|
}
|
|
80
|
+
*/
|
|
57
81
|
}
|
|
58
82
|
// File operation tools
|
|
59
83
|
export async function readFile(filePath) {
|
|
@@ -129,5 +153,5 @@ export async function getFileInfo(filePath) {
|
|
|
129
153
|
};
|
|
130
154
|
}
|
|
131
155
|
export function listAllowedDirectories() {
|
|
132
|
-
return
|
|
156
|
+
return ["/ (All paths are currently allowed)"];
|
|
133
157
|
}
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -110,6 +110,31 @@ export declare const GetFileInfoArgsSchema: z.ZodObject<{
|
|
|
110
110
|
}, {
|
|
111
111
|
path: string;
|
|
112
112
|
}>;
|
|
113
|
+
export declare const SearchCodeArgsSchema: z.ZodObject<{
|
|
114
|
+
path: z.ZodString;
|
|
115
|
+
pattern: z.ZodString;
|
|
116
|
+
filePattern: z.ZodOptional<z.ZodString>;
|
|
117
|
+
ignoreCase: z.ZodOptional<z.ZodBoolean>;
|
|
118
|
+
maxResults: z.ZodOptional<z.ZodNumber>;
|
|
119
|
+
includeHidden: z.ZodOptional<z.ZodBoolean>;
|
|
120
|
+
contextLines: z.ZodOptional<z.ZodNumber>;
|
|
121
|
+
}, "strip", z.ZodTypeAny, {
|
|
122
|
+
path: string;
|
|
123
|
+
pattern: string;
|
|
124
|
+
filePattern?: string | undefined;
|
|
125
|
+
ignoreCase?: boolean | undefined;
|
|
126
|
+
maxResults?: number | undefined;
|
|
127
|
+
includeHidden?: boolean | undefined;
|
|
128
|
+
contextLines?: number | undefined;
|
|
129
|
+
}, {
|
|
130
|
+
path: string;
|
|
131
|
+
pattern: string;
|
|
132
|
+
filePattern?: string | undefined;
|
|
133
|
+
ignoreCase?: boolean | undefined;
|
|
134
|
+
maxResults?: number | undefined;
|
|
135
|
+
includeHidden?: boolean | undefined;
|
|
136
|
+
contextLines?: number | undefined;
|
|
137
|
+
}>;
|
|
113
138
|
export declare const EditBlockArgsSchema: z.ZodObject<{
|
|
114
139
|
blockContent: z.ZodString;
|
|
115
140
|
}, "strip", z.ZodTypeAny, {
|
package/dist/tools/schemas.js
CHANGED
|
@@ -48,6 +48,16 @@ export const SearchFilesArgsSchema = z.object({
|
|
|
48
48
|
export const GetFileInfoArgsSchema = z.object({
|
|
49
49
|
path: z.string(),
|
|
50
50
|
});
|
|
51
|
+
// Search tools schema
|
|
52
|
+
export const SearchCodeArgsSchema = z.object({
|
|
53
|
+
path: z.string(),
|
|
54
|
+
pattern: z.string(),
|
|
55
|
+
filePattern: z.string().optional(),
|
|
56
|
+
ignoreCase: z.boolean().optional(),
|
|
57
|
+
maxResults: z.number().optional(),
|
|
58
|
+
includeHidden: z.boolean().optional(),
|
|
59
|
+
contextLines: z.number().optional(),
|
|
60
|
+
});
|
|
51
61
|
// Edit tools schemas
|
|
52
62
|
export const EditBlockArgsSchema = z.object({
|
|
53
63
|
blockContent: z.string(),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface SearchResult {
|
|
2
|
+
file: string;
|
|
3
|
+
line: number;
|
|
4
|
+
match: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function searchCode(options: {
|
|
7
|
+
rootPath: string;
|
|
8
|
+
pattern: string;
|
|
9
|
+
filePattern?: string;
|
|
10
|
+
ignoreCase?: boolean;
|
|
11
|
+
maxResults?: number;
|
|
12
|
+
includeHidden?: boolean;
|
|
13
|
+
contextLines?: number;
|
|
14
|
+
}): Promise<SearchResult[]>;
|
|
15
|
+
export declare function searchCodeFallback(options: {
|
|
16
|
+
rootPath: string;
|
|
17
|
+
pattern: string;
|
|
18
|
+
filePattern?: string;
|
|
19
|
+
ignoreCase?: boolean;
|
|
20
|
+
maxResults?: number;
|
|
21
|
+
excludeDirs?: string[];
|
|
22
|
+
contextLines?: number;
|
|
23
|
+
}): Promise<SearchResult[]>;
|
|
24
|
+
export declare function searchTextInFiles(options: {
|
|
25
|
+
rootPath: string;
|
|
26
|
+
pattern: string;
|
|
27
|
+
filePattern?: string;
|
|
28
|
+
ignoreCase?: boolean;
|
|
29
|
+
maxResults?: number;
|
|
30
|
+
includeHidden?: boolean;
|
|
31
|
+
contextLines?: number;
|
|
32
|
+
}): Promise<SearchResult[]>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { validatePath } from './filesystem.js';
|
|
5
|
+
import { rgPath } from '@vscode/ripgrep';
|
|
6
|
+
// Function to search file contents using ripgrep
|
|
7
|
+
export async function searchCode(options) {
|
|
8
|
+
const { rootPath, pattern, filePattern, ignoreCase = true, maxResults = 1000, includeHidden = false, contextLines = 0 } = options;
|
|
9
|
+
// Validate path for security
|
|
10
|
+
const validPath = await validatePath(rootPath);
|
|
11
|
+
// Build command arguments
|
|
12
|
+
const args = [
|
|
13
|
+
'--json', // Output in JSON format for easier parsing
|
|
14
|
+
'--line-number', // Include line numbers
|
|
15
|
+
];
|
|
16
|
+
if (ignoreCase) {
|
|
17
|
+
args.push('-i');
|
|
18
|
+
}
|
|
19
|
+
if (maxResults) {
|
|
20
|
+
args.push('-m', maxResults.toString());
|
|
21
|
+
}
|
|
22
|
+
if (includeHidden) {
|
|
23
|
+
args.push('--hidden');
|
|
24
|
+
}
|
|
25
|
+
if (contextLines > 0) {
|
|
26
|
+
args.push('-C', contextLines.toString());
|
|
27
|
+
}
|
|
28
|
+
if (filePattern) {
|
|
29
|
+
args.push('-g', filePattern);
|
|
30
|
+
}
|
|
31
|
+
// Add pattern and path
|
|
32
|
+
args.push(pattern, validPath);
|
|
33
|
+
// Run ripgrep command
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const results = [];
|
|
36
|
+
const rg = spawn(rgPath, args);
|
|
37
|
+
let stdoutBuffer = '';
|
|
38
|
+
rg.stdout.on('data', (data) => {
|
|
39
|
+
stdoutBuffer += data.toString();
|
|
40
|
+
});
|
|
41
|
+
rg.stderr.on('data', (data) => {
|
|
42
|
+
console.error(`ripgrep error: ${data}`);
|
|
43
|
+
});
|
|
44
|
+
rg.on('close', (code) => {
|
|
45
|
+
if (code === 0 || code === 1) {
|
|
46
|
+
// Process the buffered output
|
|
47
|
+
const lines = stdoutBuffer.trim().split('\n');
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (!line)
|
|
50
|
+
continue;
|
|
51
|
+
try {
|
|
52
|
+
const result = JSON.parse(line);
|
|
53
|
+
if (result.type === 'match') {
|
|
54
|
+
result.data.submatches.forEach((submatch) => {
|
|
55
|
+
results.push({
|
|
56
|
+
file: result.data.path.text,
|
|
57
|
+
line: result.data.line_number,
|
|
58
|
+
match: submatch.match.text
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else if (result.type === 'context' && contextLines > 0) {
|
|
63
|
+
results.push({
|
|
64
|
+
file: result.data.path.text,
|
|
65
|
+
line: result.data.line_number,
|
|
66
|
+
match: result.data.lines.text.trim()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// Skip non-JSON output
|
|
72
|
+
console.error('Error parsing ripgrep output:', e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
resolve(results);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
reject(new Error(`ripgrep process exited with code ${code}`));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Fallback implementation using Node.js for environments without ripgrep
|
|
84
|
+
export async function searchCodeFallback(options) {
|
|
85
|
+
const { rootPath, pattern, filePattern, ignoreCase = true, maxResults = 1000, excludeDirs = ['node_modules', '.git'], contextLines = 0 } = options;
|
|
86
|
+
const validPath = await validatePath(rootPath);
|
|
87
|
+
const results = [];
|
|
88
|
+
const regex = new RegExp(pattern, ignoreCase ? 'i' : '');
|
|
89
|
+
const fileRegex = filePattern ? new RegExp(filePattern) : null;
|
|
90
|
+
async function searchDir(dirPath) {
|
|
91
|
+
if (results.length >= maxResults)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (results.length >= maxResults)
|
|
97
|
+
break;
|
|
98
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
99
|
+
try {
|
|
100
|
+
await validatePath(fullPath);
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
if (!excludeDirs.includes(entry.name)) {
|
|
103
|
+
await searchDir(fullPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (entry.isFile()) {
|
|
107
|
+
if (!fileRegex || fileRegex.test(entry.name)) {
|
|
108
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
if (regex.test(lines[i])) {
|
|
112
|
+
// Add the matched line
|
|
113
|
+
results.push({
|
|
114
|
+
file: fullPath,
|
|
115
|
+
line: i + 1,
|
|
116
|
+
match: lines[i].trim()
|
|
117
|
+
});
|
|
118
|
+
// Add context lines
|
|
119
|
+
if (contextLines > 0) {
|
|
120
|
+
const startIdx = Math.max(0, i - contextLines);
|
|
121
|
+
const endIdx = Math.min(lines.length - 1, i + contextLines);
|
|
122
|
+
for (let j = startIdx; j <= endIdx; j++) {
|
|
123
|
+
if (j !== i) { // Skip the match line as it's already added
|
|
124
|
+
results.push({
|
|
125
|
+
file: fullPath,
|
|
126
|
+
line: j + 1,
|
|
127
|
+
match: lines[j].trim()
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (results.length >= maxResults)
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// Skip files/directories we can't access
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// Skip directories we can't read
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await searchDir(validPath);
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
// Main function that tries ripgrep first, falls back to native implementation
|
|
153
|
+
export async function searchTextInFiles(options) {
|
|
154
|
+
try {
|
|
155
|
+
return await searchCode(options);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error('Ripgrep search failed, falling back to native implementation:', error);
|
|
159
|
+
return searchCodeFallback({
|
|
160
|
+
...options,
|
|
161
|
+
excludeDirs: ['node_modules', '.git', 'dist']
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.21";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.1.
|
|
1
|
+
export const VERSION = '0.1.21';
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wonderwhy-er/desktop-commander",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "MCP server for terminal operations and file editing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eduards Ruzga",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"setup": "dist/setup-claude-server.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"logo.png",
|
|
17
|
+
"testemonials"
|
|
16
18
|
],
|
|
17
19
|
"scripts": {
|
|
18
20
|
"sync-version": "node scripts/sync-version.js",
|
|
@@ -28,7 +30,8 @@
|
|
|
28
30
|
"test:watch": "nodemon test/test.js",
|
|
29
31
|
"link:local": "npm run build && npm link",
|
|
30
32
|
"unlink:local": "npm unlink",
|
|
31
|
-
"inspector": "npx @modelcontextprotocol/inspector dist/index.js"
|
|
33
|
+
"inspector": "npx @modelcontextprotocol/inspector dist/index.js",
|
|
34
|
+
"npm-publish": "npm publish"
|
|
32
35
|
},
|
|
33
36
|
"publishConfig": {
|
|
34
37
|
"access": "public"
|
|
@@ -54,6 +57,7 @@
|
|
|
54
57
|
],
|
|
55
58
|
"dependencies": {
|
|
56
59
|
"@modelcontextprotocol/sdk": "1.0.1",
|
|
60
|
+
"@vscode/ripgrep": "^1.15.9",
|
|
57
61
|
"glob": "^10.3.10",
|
|
58
62
|
"zod": "^3.24.1",
|
|
59
63
|
"zod-to-json-schema": "^3.23.5"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|