@wonderwhy-er/desktop-commander 0.1.18 → 0.1.20

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 CHANGED
@@ -1,11 +1,31 @@
1
- # Claude Desktop Commander MCP
1
+ # Desktop Commander MCP
2
+
2
3
 
3
4
  [![npm downloads](https://img.shields.io/npm/dw/@wonderwhy-er/desktop-commander)](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
4
5
  [![smithery badge](https://smithery.ai/badge/@wonderwhy-er/desktop-commander)](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
6
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow.svg)](https://www.buymeacoffee.com/wonderwhyer)
5
7
 
8
+ [![Discord](https://img.shields.io/badge/Join%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/kQ27sNnZr7)
6
9
 
7
10
  Short version. Two key things. Terminal commands and diff based file editing.
8
11
 
12
+ ![Desktop Commander MCP](logo.png)
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
+ [![It's a life saver! I paid Claude + Cursor currently which I always feel it's kind of duplicated. This solves the problem ultimately. I am so happy. Thanks so much. Plus today Claude has added the web search support. With this MCP + Internet search, it writes the code with the latest updates. It's so good when Cursor doesn't work sometimes or all the fast requests are used.](testemonials/img.png) 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
+ [![This is the first comment I've ever left on a youtube video, THANK YOU! I've been struggling to update an old Flutter app in Cursor from an old pre null-safety version to a current version and implemented null-safety using Claude 3.7. I got most of the way but had critical BLE errors that I spent days trying to resolve with no luck. I tried Augment Code but it didn't get it either. I implemented your MCP in Claude desktop and was able to compare the old and new codebase fully, accounting for the updates in the code, and fix the issues in a couple of hours. A word of advice to people trying this, be sure to stage changes and commit when appropriate to be able to undo unwanted changes. Amazing!](testemonials/img_1.png)
198
+ https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg
199
+ )
200
+
201
+ [![Great! I just used Windsurf, bought license a week ago, for upgrading old fullstack socket project and it works many times good or ok but also many times runs away in cascade and have to revert all changes loosing hundereds of cascade tokens. In just a week down to less than 100 tokens and do not want to buy only 300 tokens for 10$. This Claude MCP ,bought claude Pro finally needed but wanted very good reason to also have next to ChatGPT, and now can code as much as I want not worrying about token cost.
202
+ Also this is much more than code editing it is much more thank you for great video!](testemonials/img_2.png)
203
+ https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg)
204
+
205
+ [![it is a great tool, thank you, I like using it, as it gives claude an ability to do surgical edits, making it more like a human developer.](testemonials/img_3.png)
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
+ [![You sir are my hero. You've pretty much summed up and described my experiences of late, much better than I could have. Cursor and Windsurf both had me frustrated to the point where I was almost yelling at my computer screen. Out of whimsy, I thought to myself why not just ask Claude directly, and haven't looked back since.
209
+ Claude first to keep my sanity in check, then if necessary, engage with other IDEs, frameworks, etc. I thought I was the only one, glad to see I'm not lol.
210
+ 33
211
+ 1](testemonials/img_4.png)
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);
@@ -1,9 +1,19 @@
1
1
  import { CommandExecutionResult, ActiveSession } from './types.js';
2
+ interface CompletedSession {
3
+ pid: number;
4
+ output: string;
5
+ exitCode: number | null;
6
+ startTime: Date;
7
+ endTime: Date;
8
+ }
2
9
  export declare class TerminalManager {
3
10
  private sessions;
11
+ private completedSessions;
4
12
  executeCommand(command: string, timeoutMs?: number): Promise<CommandExecutionResult>;
5
13
  getNewOutput(pid: number): string | null;
6
14
  forceTerminate(pid: number): boolean;
7
15
  listActiveSessions(): ActiveSession[];
16
+ listCompletedSessions(): CompletedSession[];
8
17
  }
9
18
  export declare const terminalManager: TerminalManager;
19
+ export {};
@@ -3,6 +3,7 @@ import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
3
3
  export class TerminalManager {
4
4
  constructor() {
5
5
  this.sessions = new Map();
6
+ this.completedSessions = new Map();
6
7
  }
7
8
  async executeCommand(command, timeoutMs = DEFAULT_COMMAND_TIMEOUT) {
8
9
  const process = spawn(command, [], { shell: true });
@@ -38,8 +39,21 @@ export class TerminalManager {
38
39
  isBlocked: true
39
40
  });
40
41
  }, timeoutMs);
41
- process.on('exit', () => {
42
+ process.on('exit', (code) => {
42
43
  if (process.pid) {
44
+ // Store completed session before removing active session
45
+ this.completedSessions.set(process.pid, {
46
+ pid: process.pid,
47
+ output: output + session.lastOutput, // Combine all output
48
+ exitCode: code,
49
+ startTime: session.startTime,
50
+ endTime: new Date()
51
+ });
52
+ // Keep only last 100 completed sessions
53
+ if (this.completedSessions.size > 100) {
54
+ const oldestKey = Array.from(this.completedSessions.keys())[0];
55
+ this.completedSessions.delete(oldestKey);
56
+ }
43
57
  this.sessions.delete(process.pid);
44
58
  }
45
59
  resolve({
@@ -51,13 +65,21 @@ export class TerminalManager {
51
65
  });
52
66
  }
53
67
  getNewOutput(pid) {
68
+ // First check active sessions
54
69
  const session = this.sessions.get(pid);
55
- if (!session) {
56
- return null;
70
+ if (session) {
71
+ const output = session.lastOutput;
72
+ session.lastOutput = '';
73
+ return output;
74
+ }
75
+ // Then check completed sessions
76
+ const completedSession = this.completedSessions.get(pid);
77
+ if (completedSession) {
78
+ // Format completion message with exit code and runtime
79
+ const runtime = (completedSession.endTime.getTime() - completedSession.startTime.getTime()) / 1000;
80
+ return `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s\nFinal output:\n${completedSession.output}`;
57
81
  }
58
- const output = session.lastOutput;
59
- session.lastOutput = '';
60
- return output;
82
+ return null;
61
83
  }
62
84
  forceTerminate(pid) {
63
85
  const session = this.sessions.get(pid);
@@ -86,5 +108,8 @@ export class TerminalManager {
86
108
  runtime: now.getTime() - session.startTime.getTime()
87
109
  }));
88
110
  }
111
+ listCompletedSessions() {
112
+ return Array.from(this.completedSessions.values());
113
+ }
89
114
  }
90
115
  export const terminalManager = new TerminalManager();
@@ -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
+ }
@@ -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() // User's home directory
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 allowedDirectories;
156
+ return ["/ (All paths are currently allowed)"];
133
157
  }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export declare const LongRunningTaskSchema: z.ZodObject<{
3
+ total_steps: z.ZodDefault<z.ZodNumber>;
4
+ step_duration: z.ZodDefault<z.ZodNumber>;
5
+ should_fail: z.ZodDefault<z.ZodBoolean>;
6
+ }, "strip", z.ZodTypeAny, {
7
+ total_steps: number;
8
+ step_duration: number;
9
+ should_fail: boolean;
10
+ }, {
11
+ total_steps?: number | undefined;
12
+ step_duration?: number | undefined;
13
+ should_fail?: boolean | undefined;
14
+ }>;
15
+ export declare function executeLongRunning(args: unknown, server: any, progressToken?: string): Promise<{
16
+ content: {
17
+ type: string;
18
+ text: string;
19
+ }[];
20
+ }>;
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ export const LongRunningTaskSchema = z.object({
3
+ total_steps: z.number().default(5).describe("Total number of steps to execute"),
4
+ step_duration: z.number().default(1000).describe("Duration of each step in milliseconds"),
5
+ should_fail: z.boolean().default(false).describe("Whether the task should fail midway through"),
6
+ });
7
+ export async function executeLongRunning(args, server, progressToken) {
8
+ const parsed = LongRunningTaskSchema.safeParse(args);
9
+ if (!parsed.success) {
10
+ throw new Error(`Invalid arguments for long_running_task: ${parsed.error}`);
11
+ }
12
+ const { total_steps, step_duration, should_fail } = parsed.data;
13
+ try {
14
+ for (let step = 1; step <= total_steps; step++) {
15
+ // Simulate work
16
+ await new Promise(resolve => setTimeout(resolve, step_duration));
17
+ // Send progress notification if we have a token
18
+ if (progressToken) {
19
+ await server.notification({
20
+ method: "notifications/progress",
21
+ params: {
22
+ progress: step,
23
+ total: total_steps,
24
+ progressToken,
25
+ detail: `Completed step ${step} of ${total_steps}${should_fail && step === Math.floor(total_steps / 2)
26
+ ? ' (about to fail)'
27
+ : ''}`
28
+ }
29
+ });
30
+ }
31
+ // If should_fail is true, fail halfway through
32
+ if (should_fail && step === Math.floor(total_steps / 2)) {
33
+ throw new Error("Task failed halfway through as requested");
34
+ }
35
+ }
36
+ return {
37
+ content: [{
38
+ type: "text",
39
+ text: `Long running task completed successfully after ${total_steps} steps`
40
+ }]
41
+ };
42
+ }
43
+ catch (error) {
44
+ // Send error notification if we have a token
45
+ if (progressToken) {
46
+ await server.notification({
47
+ method: "notifications/progress",
48
+ params: {
49
+ progress: Math.floor(total_steps / 2),
50
+ total: total_steps,
51
+ progressToken,
52
+ detail: error instanceof Error ? error.message : 'Unknown error',
53
+ error: true
54
+ }
55
+ });
56
+ }
57
+ throw error;
58
+ }
59
+ }
@@ -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, {
@@ -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/types.d.ts CHANGED
@@ -22,3 +22,10 @@ export interface ActiveSession {
22
22
  isBlocked: boolean;
23
23
  runtime: number;
24
24
  }
25
+ export interface CompletedSession {
26
+ pid: number;
27
+ output: string;
28
+ exitCode: number | null;
29
+ startTime: Date;
30
+ endTime: Date;
31
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.18";
1
+ export declare const VERSION = "0.1.20";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.18';
1
+ export const VERSION = '0.1.20';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "license": "MIT",
6
6
  "author": "Eduards Ruzga",
@@ -28,7 +28,8 @@
28
28
  "test:watch": "nodemon test/test.js",
29
29
  "link:local": "npm run build && npm link",
30
30
  "unlink:local": "npm unlink",
31
- "inspector": "npx @modelcontextprotocol/inspector dist/index.js"
31
+ "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
32
+ "npm-publish": "npm publish"
32
33
  },
33
34
  "publishConfig": {
34
35
  "access": "public"
@@ -54,6 +55,7 @@
54
55
  ],
55
56
  "dependencies": {
56
57
  "@modelcontextprotocol/sdk": "1.0.1",
58
+ "@vscode/ripgrep": "^1.15.9",
57
59
  "glob": "^10.3.10",
58
60
  "zod": "^3.24.1",
59
61
  "zod-to-json-schema": "^3.23.5"