@wonderwhy-er/desktop-commander 0.1.35 → 0.1.37
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 +88 -27
- package/dist/command-manager.js +1 -1
- package/dist/config-manager.d.ts +1 -0
- package/dist/config-manager.js +21 -4
- package/dist/config.d.ts +2 -2
- package/dist/config.js +2 -3
- package/dist/error-handlers.js +1 -1
- package/dist/handlers/edit-search-handlers.d.ts +3 -1
- package/dist/handlers/edit-search-handlers.js +6 -12
- package/dist/handlers/filesystem-handlers.js +1 -1
- package/dist/index.js +1 -1
- 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/server.js +71 -43
- package/dist/setup-claude-server.js +549 -288
- package/dist/terminal-manager.js +4 -2
- package/dist/tools/edit.d.ts +8 -6
- package/dist/tools/edit.js +161 -34
- package/dist/tools/execute.js +2 -2
- package/dist/tools/filesystem.js +59 -10
- 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 +12 -3
- package/dist/tools/schemas.js +5 -2
- 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 +10 -1
- package/dist/utils.js +99 -26
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024 Eduard Ruzga
|
|
3
|
+
Copyright (c) 2024-2025 Eduard Ruzga and Desktop Commander Contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://discord.gg/kQ27sNnZr7)
|
|
10
10
|
|
|
11
|
-
[](https://www.producthunt.com/posts/desktop-commander-mcp)
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
Work with code and text, run processes, and automate tasks, going far beyond other AI editors - without API token costs.
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-

|
|
16
|
+
|
|
17
17
|
<a href="https://glama.ai/mcp/servers/zempur9oh4">
|
|
18
18
|
<img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Desktop Commander MCP" />
|
|
19
19
|
</a>
|
|
@@ -24,13 +24,17 @@ Short version. Four key things. Terminal commands, diff based file editing, ripg
|
|
|
24
24
|
- [Usage](#usage)
|
|
25
25
|
- [Handling Long-Running Commands](#handling-long-running-commands)
|
|
26
26
|
- [Work in Progress and TODOs](#work-in-progress-and-todos)
|
|
27
|
-
- [
|
|
27
|
+
- [Sponsors and Supporters](#sponsors-and-supporters)
|
|
28
|
+
- [Website](#website)
|
|
29
|
+
- [Media](#media)
|
|
28
30
|
- [Testimonials](#testimonials)
|
|
29
31
|
- [Frequently Asked Questions](#frequently-asked-questions)
|
|
30
32
|
- [Contributing](#contributing)
|
|
31
33
|
- [License](#license)
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
All of your AI development tools in one place.
|
|
36
|
+
Desktop Commander puts all dev tools in one chat.
|
|
37
|
+
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.
|
|
34
38
|
|
|
35
39
|
## Features
|
|
36
40
|
|
|
@@ -48,7 +52,7 @@ This is server that allows Claude desktop app to execute long-running terminal c
|
|
|
48
52
|
- Move files/directories
|
|
49
53
|
- Search files
|
|
50
54
|
- Get file metadata
|
|
51
|
-
|
|
55
|
+
- Code editing capabilities:
|
|
52
56
|
- Surgical text replacements for small changes
|
|
53
57
|
- Full file rewrites for major changes
|
|
54
58
|
- Multiple file support
|
|
@@ -59,7 +63,7 @@ This is server that allows Claude desktop app to execute long-running terminal c
|
|
|
59
63
|
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).
|
|
60
64
|
|
|
61
65
|
### Option 1: Install through npx
|
|
62
|
-
Just run this in terminal
|
|
66
|
+
Just run this in terminal:
|
|
63
67
|
```
|
|
64
68
|
npx @wonderwhy-er/desktop-commander@latest setup
|
|
65
69
|
```
|
|
@@ -68,7 +72,7 @@ For debugging mode (allows Node.js inspector connection):
|
|
|
68
72
|
```
|
|
69
73
|
npx @wonderwhy-er/desktop-commander@latest setup --debug
|
|
70
74
|
```
|
|
71
|
-
Restart Claude if running
|
|
75
|
+
Restart Claude if running.
|
|
72
76
|
|
|
73
77
|
### Option 2: Using bash script installer (macOS)
|
|
74
78
|
For macOS users, you can use our automated bash installer which will check your Node.js version, install it if needed, and automatically configure Desktop Commander:
|
|
@@ -85,7 +89,7 @@ To install Desktop Commander for Claude Desktop automatically via [Smithery](htt
|
|
|
85
89
|
npx -y @smithery/cli install @wonderwhy-er/desktop-commander --client claude
|
|
86
90
|
```
|
|
87
91
|
|
|
88
|
-
### Option 4: Add to claude_desktop_config
|
|
92
|
+
### Option 4: Add to claude_desktop_config manually
|
|
89
93
|
Add this entry to your claude_desktop_config.json:
|
|
90
94
|
|
|
91
95
|
- On Mac: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
|
|
@@ -105,7 +109,7 @@ Add this entry to your claude_desktop_config.json:
|
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
```
|
|
108
|
-
Restart Claude if running
|
|
112
|
+
Restart Claude if running.
|
|
109
113
|
|
|
110
114
|
### Option 5: Checkout locally
|
|
111
115
|
1. Clone and build:
|
|
@@ -114,7 +118,7 @@ git clone https://github.com/wonderwhy-er/DesktopCommanderMCP.git
|
|
|
114
118
|
cd DesktopCommanderMCP
|
|
115
119
|
npm run setup
|
|
116
120
|
```
|
|
117
|
-
Restart Claude if running
|
|
121
|
+
Restart Claude if running.
|
|
118
122
|
|
|
119
123
|
The setup command will:
|
|
120
124
|
- Install dependencies
|
|
@@ -124,7 +128,7 @@ The setup command will:
|
|
|
124
128
|
|
|
125
129
|
### Updating Desktop Commander
|
|
126
130
|
|
|
127
|
-
When installed through npx (Option 1) or Smithery (Option
|
|
131
|
+
When installed through npx (Option 1) or Smithery (Option 3), Desktop Commander will automatically update to the latest version whenever you restart Claude. No manual update process is needed.
|
|
128
132
|
|
|
129
133
|
For manual installations, you can update by running the setup command again.
|
|
130
134
|
|
|
@@ -193,7 +197,7 @@ For commands that may take a while:
|
|
|
193
197
|
|
|
194
198
|
### ⚠️ Important Security Warnings
|
|
195
199
|
|
|
196
|
-
1. **Always change configuration in a separate chat window** from where you're doing your actual work. Claude may sometimes attempt to modify configuration settings (like `allowedDirectories`) if it encounters filesystem access restrictions
|
|
200
|
+
1. **Always change configuration in a separate chat window** from where you're doing your actual work. Claude may sometimes attempt to modify configuration settings (like `allowedDirectories`) if it encounters filesystem access restrictions.
|
|
197
201
|
|
|
198
202
|
2. **The `allowedDirectories` setting currently only restricts filesystem operations**, not terminal commands. Terminal commands can still access files outside allowed directories. Full terminal sandboxing is on the roadmap.
|
|
199
203
|
|
|
@@ -293,6 +297,8 @@ This project extends the MCP Filesystem Server to enable:
|
|
|
293
297
|
Created as part of exploring Claude MCPs: https://youtube.com/live/TlbjFDbl5Us
|
|
294
298
|
|
|
295
299
|
## DONE
|
|
300
|
+
- **29-04-2025 Telemetry Opt Out trough configuraton** - There is now setting to disable telemetry in config, ask in chat
|
|
301
|
+
- **23-04-2025 Enhanced edit functionality** - Improved format, added fuzzy search and multi-occurrence replacements, should fail less and use edit block more often
|
|
296
302
|
- **16-04-2025 Better configurations** - Improved settings for allowed paths, commands and shell environments
|
|
297
303
|
- **14-04-2025 Windows environment fixes** - Resolved issues specific to Windows platforms
|
|
298
304
|
- **14-04-2025 Linux improvements** - Enhanced compatibility with various Linux distributions
|
|
@@ -303,21 +309,78 @@ Terminal still can access files ignoring allowed directories.
|
|
|
303
309
|
- **28-03-2025 Fixed "Watching /" JSON error** - Implemented custom stdio transport to handle non-JSON messages and prevent server crashes
|
|
304
310
|
- **25-03-2025 Better code search** ([merged](https://github.com/wonderwhy-er/ClaudeServerCommander/pull/17)) - Enhanced code exploration with context-aware results
|
|
305
311
|
|
|
306
|
-
## Work in Progress
|
|
312
|
+
## Work in Progress/TODOs/Roadmap
|
|
307
313
|
|
|
308
314
|
The following features are currently being explored:
|
|
309
315
|
|
|
310
316
|
- **Support for WSL** - Windows Subsystem for Linux integration
|
|
311
317
|
- **Support for SSH** - Remote server command execution
|
|
312
|
-
- **Better file support like
|
|
318
|
+
- **Better file support for formats like CSV/PDF**
|
|
313
319
|
- **Terminal sandboxing for Mac/Linux/Windows for better security**
|
|
314
|
-
- **File reading modes** -
|
|
320
|
+
- **File reading modes** - For example, allow reading HTML as plain text or markdown
|
|
321
|
+
- **Interactive shell support** - ssh, node/python repl
|
|
322
|
+
- **Improve large file reading and writing**
|
|
323
|
+
|
|
324
|
+
## ❤️ Support Desktop Commander
|
|
325
|
+
|
|
326
|
+
<div align="center">
|
|
327
|
+
<h3>📢 SUPPORT THIS PROJECT</h3>
|
|
328
|
+
<p><strong>Desktop Commander MCP is free and open source, but needs your support to thrive!</strong></p>
|
|
329
|
+
|
|
330
|
+
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 10px; margin: 20px 0; border: 2px solid #007bff;">
|
|
331
|
+
<p>Our philosophy is simple: we don't want you to pay for it if you're not successful. But if Desktop Commander contributes to your success, please consider contributing to ours.</p>
|
|
332
|
+
<p><strong>Ways to support:</strong></p>
|
|
333
|
+
<ul style="list-style-type: none; padding: 0;">
|
|
334
|
+
<li>🌟 <a href="https://github.com/sponsors/wonderwhy-er"><strong>GitHub Sponsors</strong></a> - Recurring support</li>
|
|
335
|
+
<li>☕ <a href="https://www.buymeacoffee.com/wonderwhyer"><strong>Buy Me A Coffee</strong></a> - One-time contributions</li>
|
|
336
|
+
<li>⭐ <a href="https://github.com/wonderwhy-er/DesktopCommanderMCP"><strong>Star on GitHub</strong></a> - Help others discover the project</li>
|
|
337
|
+
</ul>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
### Supporters Hall of Fame
|
|
342
|
+
|
|
343
|
+
Generous supporters are featured here. Thank you for helping make this project possible!
|
|
344
|
+
|
|
345
|
+
<div align="center">
|
|
346
|
+
<table>
|
|
347
|
+
<tr>
|
|
348
|
+
<td align="center">
|
|
349
|
+
<a href="https://github.com/jonrichards">
|
|
350
|
+
<img src="https://github.com/jonrichards.png" width="100px;" alt="Jon Richards"/>
|
|
351
|
+
<br />
|
|
352
|
+
<sub><b>Jon Richards</b></sub>
|
|
353
|
+
</a>
|
|
354
|
+
</td>
|
|
355
|
+
<td align="center">
|
|
356
|
+
<a href="https://github.com/stepanic">
|
|
357
|
+
<img src="https://github.com/stepanic.png" width="100px;" alt="Matija Stepanic"/>
|
|
358
|
+
<br />
|
|
359
|
+
<sub><b>Matija Stepanic</b></sub>
|
|
360
|
+
</a>
|
|
361
|
+
</td>
|
|
362
|
+
</tr>
|
|
363
|
+
</table>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<details>
|
|
367
|
+
<summary><strong>Why your support matters</strong></summary>
|
|
368
|
+
<p>Your support allows us to:</p>
|
|
369
|
+
<ul>
|
|
370
|
+
<li>Continue active development and maintenance</li>
|
|
371
|
+
<li>Add new features and integrations</li>
|
|
372
|
+
<li>Improve compatibility across platforms</li>
|
|
373
|
+
<li>Provide better documentation and examples</li>
|
|
374
|
+
<li>Build a stronger community around the project</li>
|
|
375
|
+
</ul>
|
|
376
|
+
</details>
|
|
315
377
|
|
|
316
378
|
## Website
|
|
317
379
|
|
|
318
380
|
Visit our official website at [https://desktopcommander.app/](https://desktopcommander.app/) for the latest information, documentation, and updates.
|
|
319
381
|
|
|
320
382
|
## Media
|
|
383
|
+
|
|
321
384
|
Learn more about this project through these resources:
|
|
322
385
|
|
|
323
386
|
### Article
|
|
@@ -356,8 +419,6 @@ Claude first to keep my sanity in check, then if necessary, engage with other ID
|
|
|
356
419
|
1](https://raw.githubusercontent.com/wonderwhy-er/ClaudeComputerCommander/main/testemonials/img_4.png)
|
|
357
420
|
https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e](https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e)
|
|
358
421
|
|
|
359
|
-
## Contributing
|
|
360
|
-
|
|
361
422
|
If you find this project useful, please consider giving it a ⭐ star on GitHub! This helps others discover the project and encourages further development.
|
|
362
423
|
|
|
363
424
|
We welcome contributions from the community! Whether you've found a bug, have a feature request, or want to contribute code, here's how you can help:
|
|
@@ -375,7 +436,7 @@ If you find this tool valuable for your workflow, please consider [supporting th
|
|
|
375
436
|
|
|
376
437
|
Here are answers to some common questions. For a more comprehensive FAQ, see our [detailed FAQ document](FAQ.md).
|
|
377
438
|
|
|
378
|
-
### What is
|
|
439
|
+
### What is Desktop Commander?
|
|
379
440
|
It's an MCP tool that enables Claude Desktop to access your file system and terminal, turning Claude into a versatile assistant for coding, automation, codebase exploration, and more.
|
|
380
441
|
|
|
381
442
|
### How is this different from Cursor/Windsurf?
|
|
@@ -397,17 +458,17 @@ Yes, when installed through npx or Smithery, Desktop Commander automatically upd
|
|
|
397
458
|
### I'm having trouble installing or using the tool. Where can I get help?
|
|
398
459
|
Join our [Discord server](https://discord.gg/kQ27sNnZr7) for community support, check the [GitHub issues](https://github.com/wonderwhy-er/DesktopCommanderMCP/issues) for known problems, or review the [full FAQ](FAQ.md) for troubleshooting tips. You can also visit our [website FAQ section](https://desktopcommander.app#faq) for a more user-friendly experience. If you encounter a new issue, please consider [opening a GitHub issue](https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/new) with details about your problem.
|
|
399
460
|
|
|
400
|
-
## Data Collection
|
|
461
|
+
## Data Collection & Privacy
|
|
462
|
+
|
|
463
|
+
Desktop Commander collects limited anonymous telemetry data to help improve the tool. No personal information, file contents, file paths, or command arguments are collected.
|
|
401
464
|
|
|
402
|
-
|
|
403
|
-
- Operating system information
|
|
404
|
-
- Node.js and NPM versions
|
|
405
|
-
- Installation method and shell environment
|
|
406
|
-
- Error messages (if any occur during setup)
|
|
465
|
+
Telemetry is enabled by default. To opt out:
|
|
407
466
|
|
|
408
|
-
|
|
467
|
+
1. Open the chat and simply ask:
|
|
468
|
+
**"Disable telemetry"**
|
|
469
|
+
2. The chatbot will update your settings automatically.
|
|
409
470
|
|
|
410
|
-
|
|
471
|
+
For complete details about data collection, please see our [Privacy Policy](PRIVACY.md).
|
|
411
472
|
|
|
412
473
|
## License
|
|
413
474
|
|
package/dist/command-manager.js
CHANGED
package/dist/config-manager.d.ts
CHANGED
package/dist/config-manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { mkdir } from 'fs/promises';
|
|
5
5
|
import os from 'os';
|
|
6
|
+
import { CONFIG_FILE } from './config.js';
|
|
6
7
|
/**
|
|
7
8
|
* Singleton config manager for the server
|
|
8
9
|
*/
|
|
@@ -11,10 +12,8 @@ class ConfigManager {
|
|
|
11
12
|
this.config = {};
|
|
12
13
|
this.initialized = false;
|
|
13
14
|
// Get user's home directory
|
|
14
|
-
const homeDir = os.homedir();
|
|
15
15
|
// Define config directory and file paths
|
|
16
|
-
|
|
17
|
-
this.configPath = path.join(configDir, 'config.json');
|
|
16
|
+
this.configPath = CONFIG_FILE;
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
19
|
* Initialize configuration - load from disk or create default
|
|
@@ -101,7 +100,8 @@ class ConfigManager {
|
|
|
101
100
|
"takeown" // Take ownership of files
|
|
102
101
|
],
|
|
103
102
|
defaultShell: os.platform() === 'win32' ? 'powershell.exe' : 'bash',
|
|
104
|
-
allowedDirectories: []
|
|
103
|
+
allowedDirectories: [],
|
|
104
|
+
telemetryEnabled: true // Default to opt-out approach (telemetry on by default)
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
@@ -135,6 +135,23 @@ class ConfigManager {
|
|
|
135
135
|
*/
|
|
136
136
|
async setValue(key, value) {
|
|
137
137
|
await this.init();
|
|
138
|
+
// Special handling for telemetry opt-out
|
|
139
|
+
if (key === 'telemetryEnabled' && value === false) {
|
|
140
|
+
// Get the current value before changing it
|
|
141
|
+
const currentValue = this.config[key];
|
|
142
|
+
// Only capture the opt-out event if telemetry was previously enabled
|
|
143
|
+
if (currentValue !== false) {
|
|
144
|
+
// Import the capture function dynamically to avoid circular dependencies
|
|
145
|
+
const { capture } = await import('./utils/capture.js');
|
|
146
|
+
// Send a final telemetry event noting that the user has opted out
|
|
147
|
+
// This helps us track opt-out rates while respecting the user's choice
|
|
148
|
+
await capture('server_telemetry_opt_out', {
|
|
149
|
+
reason: 'user_disabled',
|
|
150
|
+
prev_value: currentValue
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Update the value
|
|
138
155
|
this.config[key] = value;
|
|
139
156
|
await this.saveConfig();
|
|
140
157
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
+
export declare const USER_HOME: string;
|
|
1
2
|
export declare const CONFIG_FILE: string;
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const ERROR_LOG_FILE: string;
|
|
3
|
+
export declare const TOOL_CALL_FILE: string;
|
|
4
4
|
export declare const DEFAULT_COMMAND_TIMEOUT = 1000;
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
// Use user's home directory for configuration files
|
|
4
|
-
const USER_HOME = os.homedir();
|
|
4
|
+
export const USER_HOME = os.homedir();
|
|
5
5
|
const CONFIG_DIR = path.join(USER_HOME, '.claude-server-commander');
|
|
6
6
|
// Paths relative to the config directory
|
|
7
7
|
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
8
|
-
export const
|
|
9
|
-
export const ERROR_LOG_FILE = path.join(CONFIG_DIR, 'error.log');
|
|
8
|
+
export const TOOL_CALL_FILE = path.join(CONFIG_DIR, 'claude_tool_call.log');
|
|
10
9
|
export const DEFAULT_COMMAND_TIMEOUT = 1000; // milliseconds
|
package/dist/error-handlers.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { handleEditBlock } from '../tools/edit.js';
|
|
1
2
|
import { ServerResult } from '../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Handle edit_block command
|
|
5
|
+
* Uses the enhanced implementation with multiple occurrence support and fuzzy matching
|
|
4
6
|
*/
|
|
5
|
-
export
|
|
7
|
+
export { handleEditBlock };
|
|
6
8
|
/**
|
|
7
9
|
* Handle search_code command
|
|
8
10
|
*/
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import { parseEditBlock, performSearchReplace } from '../tools/edit.js';
|
|
2
1
|
import { searchTextInFiles } from '../tools/search.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { SearchCodeArgsSchema } from '../tools/schemas.js';
|
|
3
|
+
import { handleEditBlock } from '../tools/edit.js';
|
|
4
|
+
import { capture } from '../utils/capture.js';
|
|
5
|
+
import { withTimeout } from '../utils/withTimeout.js';
|
|
6
6
|
/**
|
|
7
7
|
* Handle edit_block command
|
|
8
|
+
* Uses the enhanced implementation with multiple occurrence support and fuzzy matching
|
|
8
9
|
*/
|
|
9
|
-
export
|
|
10
|
-
const parsed = EditBlockArgsSchema.parse(args);
|
|
11
|
-
const { filePath, searchReplace, error } = await parseEditBlock(parsed.blockContent);
|
|
12
|
-
if (error) {
|
|
13
|
-
return createErrorResponse(error);
|
|
14
|
-
}
|
|
15
|
-
return performSearchReplace(filePath, searchReplace);
|
|
16
|
-
}
|
|
10
|
+
export { handleEditBlock };
|
|
17
11
|
/**
|
|
18
12
|
* Handle search_code command
|
|
19
13
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, readMultipleFiles, writeFile, createDirectory, listDirectory, moveFile, searchFiles, getFileInfo } from '../tools/filesystem.js';
|
|
2
|
-
import { withTimeout } from '../utils.js';
|
|
2
|
+
import { withTimeout } from '../utils/withTimeout.js';
|
|
3
3
|
import { createErrorResponse } from '../error-handlers.js';
|
|
4
4
|
import { ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, SearchFilesArgsSchema, GetFileInfoArgsSchema } from '../tools/schemas.js';
|
|
5
5
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { configManager } from './config-manager.js';
|
|
|
5
5
|
import { join, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
7
|
import { platform } from 'os';
|
|
8
|
-
import { capture } from './utils.js';
|
|
8
|
+
import { capture } from './utils/capture.js';
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
const isWindows = platform() === 'win32';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: PolyForm-Small-Business-1.0.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Desktope Commander MCP Contributors
|
|
5
|
+
*
|
|
6
|
+
* This file is licensed under the PolyForm Small Business License 1.0.0
|
|
7
|
+
* See the LICENSE file in the /src/polyform directory for the full license text.
|
|
8
|
+
*/
|
|
9
|
+
import { ServerResult } from '../../types.js';
|
|
10
|
+
interface SearchReplace {
|
|
11
|
+
search: string;
|
|
12
|
+
replace: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function performSearchReplace(filePath: string, block: SearchReplace, expectedReplacements?: number): Promise<ServerResult>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: PolyForm-Small-Business-1.0.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Desktope Commander MCP Contributors
|
|
5
|
+
*
|
|
6
|
+
* This file is licensed under the PolyForm Small Business License 1.0.0
|
|
7
|
+
* See the LICENSE file in the /src/polyform directory for the full license text.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, writeFile } from '../../tools/filesystem.js';
|
|
10
|
+
import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
|
|
11
|
+
import { capture } from '../../utils.js';
|
|
12
|
+
/**
|
|
13
|
+
* Threshold for fuzzy matching - similarity must be at least this value to be considered
|
|
14
|
+
* (0-1 scale where 1 is perfect match and 0 is completely different)
|
|
15
|
+
*/
|
|
16
|
+
const FUZZY_THRESHOLD = 0.7;
|
|
17
|
+
export async function performSearchReplace(filePath, block, expectedReplacements = 1) {
|
|
18
|
+
// Check for empty search string to prevent infinite loops
|
|
19
|
+
if (block.search === "") {
|
|
20
|
+
return {
|
|
21
|
+
content: [{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: "Empty search strings are not allowed. Please provide a non-empty string to search for."
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Read file as plain string
|
|
28
|
+
const { content } = await readFile(filePath);
|
|
29
|
+
// Make sure content is a string
|
|
30
|
+
if (typeof content !== 'string') {
|
|
31
|
+
throw new Error('Wrong content for file ' + filePath);
|
|
32
|
+
}
|
|
33
|
+
// First try exact match
|
|
34
|
+
let tempContent = content;
|
|
35
|
+
let count = 0;
|
|
36
|
+
let pos = tempContent.indexOf(block.search);
|
|
37
|
+
while (pos !== -1) {
|
|
38
|
+
count++;
|
|
39
|
+
pos = tempContent.indexOf(block.search, pos + 1);
|
|
40
|
+
}
|
|
41
|
+
// If exact match found and count matches expected replacements, proceed with exact replacement
|
|
42
|
+
if (count > 0 && count === expectedReplacements) {
|
|
43
|
+
// Replace all occurrences
|
|
44
|
+
let newContent = content;
|
|
45
|
+
// If we're only replacing one occurrence, replace it directly
|
|
46
|
+
if (expectedReplacements === 1) {
|
|
47
|
+
const searchIndex = newContent.indexOf(block.search);
|
|
48
|
+
newContent =
|
|
49
|
+
newContent.substring(0, searchIndex) +
|
|
50
|
+
block.replace +
|
|
51
|
+
newContent.substring(searchIndex + block.search.length);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Replace all occurrences using split and join for multiple replacements
|
|
55
|
+
newContent = newContent.split(block.search).join(block.replace);
|
|
56
|
+
}
|
|
57
|
+
await writeFile(filePath, newContent);
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Successfully applied ${expectedReplacements} edit${expectedReplacements > 1 ? 's' : ''} to ${filePath}`
|
|
62
|
+
}],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// If exact match found but count doesn't match expected, inform the user
|
|
66
|
+
if (count > 0 && count !== expectedReplacements) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: `Expected ${expectedReplacements} occurrences but found ${count} in ${filePath}. ` +
|
|
71
|
+
`If you want to replace all ${count} occurrences, set expected_replacements to ${count}. ` +
|
|
72
|
+
`If you want to replace a specific occurrence, make your search string more unique by adding context.`
|
|
73
|
+
}],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// If exact match not found, try fuzzy search
|
|
77
|
+
if (count === 0) {
|
|
78
|
+
// Track fuzzy search time
|
|
79
|
+
const startTime = performance.now();
|
|
80
|
+
// Perform fuzzy search
|
|
81
|
+
const fuzzyResult = recursiveFuzzyIndexOf(content, block.search);
|
|
82
|
+
const similarity = getSimilarityRatio(block.search, fuzzyResult.value);
|
|
83
|
+
// Calculate execution time in milliseconds
|
|
84
|
+
const executionTime = performance.now() - startTime;
|
|
85
|
+
// Check if the fuzzy match is "close enough"
|
|
86
|
+
if (similarity >= FUZZY_THRESHOLD) {
|
|
87
|
+
// Format differences for clearer output
|
|
88
|
+
const diff = highlightDifferences(block.search, fuzzyResult.value);
|
|
89
|
+
// Capture the fuzzy search event
|
|
90
|
+
capture('server_fuzzy_search_performed', {
|
|
91
|
+
similarity: similarity,
|
|
92
|
+
execution_time_ms: executionTime,
|
|
93
|
+
search_length: block.search.length,
|
|
94
|
+
file_size: content.length,
|
|
95
|
+
threshold: FUZZY_THRESHOLD,
|
|
96
|
+
found_text_length: fuzzyResult.value.length
|
|
97
|
+
});
|
|
98
|
+
// If we allow fuzzy matches, we would make the replacement here
|
|
99
|
+
// For now, we'll return a detailed message about the fuzzy match
|
|
100
|
+
return {
|
|
101
|
+
content: [{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: `Exact match not found, but found a similar text with ${Math.round(similarity * 100)}% similarity (found in ${executionTime.toFixed(2)}ms):\n\n` +
|
|
104
|
+
`Differences:\n${diff}\n\n` +
|
|
105
|
+
`To replace this text, use the exact text found in the file.`
|
|
106
|
+
}],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// If the fuzzy match isn't close enough
|
|
111
|
+
// Still capture the fuzzy search event even for unsuccessful matches
|
|
112
|
+
capture('server_fuzzy_search_performed', {
|
|
113
|
+
similarity: similarity,
|
|
114
|
+
execution_time_ms: executionTime,
|
|
115
|
+
search_length: block.search.length,
|
|
116
|
+
file_size: content.length,
|
|
117
|
+
threshold: FUZZY_THRESHOLD,
|
|
118
|
+
found_text_length: fuzzyResult.value.length,
|
|
119
|
+
file_path: filePath,
|
|
120
|
+
below_threshold: true
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: `Search content not found in ${filePath}. The closest match was "${fuzzyResult.value}" ` +
|
|
126
|
+
`with only ${Math.round(similarity * 100)}% similarity, which is below the ${Math.round(FUZZY_THRESHOLD * 100)}% threshold. ` +
|
|
127
|
+
`(Fuzzy search completed in ${executionTime.toFixed(2)}ms)`
|
|
128
|
+
}],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw new Error("Unexpected error during search and replace operation.");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Generates a character-level diff using standard {-removed-}{+added+} format
|
|
136
|
+
* @param expected The string that was searched for
|
|
137
|
+
* @param actual The string that was found
|
|
138
|
+
* @returns A formatted string showing character-level differences
|
|
139
|
+
*/
|
|
140
|
+
function highlightDifferences(expected, actual) {
|
|
141
|
+
// Implementation of a simplified character-level diff
|
|
142
|
+
// Find common prefix and suffix
|
|
143
|
+
let prefixLength = 0;
|
|
144
|
+
const minLength = Math.min(expected.length, actual.length);
|
|
145
|
+
// Determine common prefix length
|
|
146
|
+
while (prefixLength < minLength &&
|
|
147
|
+
expected[prefixLength] === actual[prefixLength]) {
|
|
148
|
+
prefixLength++;
|
|
149
|
+
}
|
|
150
|
+
// Determine common suffix length
|
|
151
|
+
let suffixLength = 0;
|
|
152
|
+
while (suffixLength < minLength - prefixLength &&
|
|
153
|
+
expected[expected.length - 1 - suffixLength] === actual[actual.length - 1 - suffixLength]) {
|
|
154
|
+
suffixLength++;
|
|
155
|
+
}
|
|
156
|
+
// Extract the common and different parts
|
|
157
|
+
const commonPrefix = expected.substring(0, prefixLength);
|
|
158
|
+
const commonSuffix = expected.substring(expected.length - suffixLength);
|
|
159
|
+
const expectedDiff = expected.substring(prefixLength, expected.length - suffixLength);
|
|
160
|
+
const actualDiff = actual.substring(prefixLength, actual.length - suffixLength);
|
|
161
|
+
// Format the output as a character-level diff
|
|
162
|
+
return `${commonPrefix}{-${expectedDiff}-}{+${actualDiff}+}${commonSuffix}`;
|
|
163
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: PolyForm-Small-Business-1.0.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 Desktope Commander MCP Contributors
|
|
5
|
+
*
|
|
6
|
+
* This file is licensed under the PolyForm Small Business License 1.0.0
|
|
7
|
+
* See the LICENSE file in the /src/polyform directory for the full license text.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Recursively finds the closest match to a query string within text using fuzzy matching
|
|
11
|
+
* @param text The text to search within
|
|
12
|
+
* @param query The query string to find
|
|
13
|
+
* @param start Start index in the text (default: 0)
|
|
14
|
+
* @param end End index in the text (default: text.length)
|
|
15
|
+
* @param parentDistance Best distance found so far (default: Infinity)
|
|
16
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
17
|
+
*/
|
|
18
|
+
export declare function recursiveFuzzyIndexOf(text: string, query: string, start?: number, end?: number | null, parentDistance?: number, depth?: number): {
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
value: string;
|
|
22
|
+
distance: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Calculates the similarity ratio between two strings
|
|
26
|
+
* @param a First string
|
|
27
|
+
* @param b Second string
|
|
28
|
+
* @returns Similarity ratio (0-1)
|
|
29
|
+
*/
|
|
30
|
+
export declare function getSimilarityRatio(a: string, b: string): number;
|