@wonderwhy-er/desktop-commander 0.2.23 → 0.2.25

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.
Files changed (70) hide show
  1. package/README.md +14 -55
  2. package/dist/config-manager.d.ts +5 -0
  3. package/dist/config-manager.js +9 -0
  4. package/dist/custom-stdio.d.ts +1 -0
  5. package/dist/custom-stdio.js +19 -0
  6. package/dist/handlers/filesystem-handlers.d.ts +4 -0
  7. package/dist/handlers/filesystem-handlers.js +120 -14
  8. package/dist/handlers/node-handlers.d.ts +6 -0
  9. package/dist/handlers/node-handlers.js +73 -0
  10. package/dist/index.js +5 -3
  11. package/dist/search-manager.d.ts +25 -0
  12. package/dist/search-manager.js +212 -0
  13. package/dist/server.d.ts +11 -0
  14. package/dist/server.js +188 -73
  15. package/dist/terminal-manager.d.ts +56 -2
  16. package/dist/terminal-manager.js +169 -13
  17. package/dist/tools/edit.d.ts +28 -4
  18. package/dist/tools/edit.js +87 -4
  19. package/dist/tools/filesystem.d.ts +23 -12
  20. package/dist/tools/filesystem.js +201 -416
  21. package/dist/tools/improved-process-tools.d.ts +2 -2
  22. package/dist/tools/improved-process-tools.js +244 -214
  23. package/dist/tools/mime-types.d.ts +1 -0
  24. package/dist/tools/mime-types.js +7 -0
  25. package/dist/tools/pdf/extract-images.d.ts +34 -0
  26. package/dist/tools/pdf/extract-images.js +132 -0
  27. package/dist/tools/pdf/index.d.ts +6 -0
  28. package/dist/tools/pdf/index.js +3 -0
  29. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  30. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  31. package/dist/tools/pdf/manipulations.d.ts +13 -0
  32. package/dist/tools/pdf/manipulations.js +96 -0
  33. package/dist/tools/pdf/markdown.d.ts +7 -0
  34. package/dist/tools/pdf/markdown.js +37 -0
  35. package/dist/tools/pdf/utils.d.ts +12 -0
  36. package/dist/tools/pdf/utils.js +34 -0
  37. package/dist/tools/schemas.d.ts +167 -12
  38. package/dist/tools/schemas.js +54 -5
  39. package/dist/types.d.ts +2 -1
  40. package/dist/utils/ab-test.d.ts +8 -0
  41. package/dist/utils/ab-test.js +76 -0
  42. package/dist/utils/capture.js +5 -0
  43. package/dist/utils/feature-flags.js +7 -4
  44. package/dist/utils/files/base.d.ts +167 -0
  45. package/dist/utils/files/base.js +5 -0
  46. package/dist/utils/files/binary.d.ts +21 -0
  47. package/dist/utils/files/binary.js +65 -0
  48. package/dist/utils/files/excel.d.ts +24 -0
  49. package/dist/utils/files/excel.js +416 -0
  50. package/dist/utils/files/factory.d.ts +40 -0
  51. package/dist/utils/files/factory.js +101 -0
  52. package/dist/utils/files/image.d.ts +21 -0
  53. package/dist/utils/files/image.js +78 -0
  54. package/dist/utils/files/index.d.ts +10 -0
  55. package/dist/utils/files/index.js +13 -0
  56. package/dist/utils/files/pdf.d.ts +32 -0
  57. package/dist/utils/files/pdf.js +142 -0
  58. package/dist/utils/files/text.d.ts +63 -0
  59. package/dist/utils/files/text.js +357 -0
  60. package/dist/utils/open-browser.d.ts +9 -0
  61. package/dist/utils/open-browser.js +43 -0
  62. package/dist/utils/ripgrep-resolver.js +3 -2
  63. package/dist/utils/system-info.d.ts +5 -0
  64. package/dist/utils/system-info.js +71 -3
  65. package/dist/utils/usageTracker.js +6 -0
  66. package/dist/utils/welcome-onboarding.d.ts +9 -0
  67. package/dist/utils/welcome-onboarding.js +37 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +14 -3
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Text file handler
3
+ * Handles reading, writing, and editing text files
4
+ *
5
+ * Binary detection is handled at the factory level (factory.ts) using isBinaryFile.
6
+ * This handler only receives files that have been confirmed as text.
7
+ *
8
+ * TECHNICAL DEBT:
9
+ * This handler is missing editRange() - text search/replace logic currently lives in
10
+ * src/tools/edit.ts (performSearchReplace function) instead of here.
11
+ *
12
+ * For architectural consistency with ExcelFileHandler.editRange(), the fuzzy
13
+ * search/replace logic should be moved here. See comment in src/tools/edit.ts.
14
+ */
15
+ import fs from "fs/promises";
16
+ import { createReadStream } from 'fs';
17
+ import { createInterface } from 'readline';
18
+ // TODO: Centralize these constants with filesystem.ts to avoid silent drift
19
+ // These duplicate concepts from filesystem.ts and should be moved to a shared
20
+ // constants module (e.g., src/utils/files/constants.ts) during reorganization
21
+ const FILE_SIZE_LIMITS = {
22
+ LARGE_FILE_THRESHOLD: 10 * 1024 * 1024, // 10MB
23
+ LINE_COUNT_LIMIT: 10 * 1024 * 1024, // 10MB for line counting
24
+ };
25
+ const READ_PERFORMANCE_THRESHOLDS = {
26
+ SMALL_READ_THRESHOLD: 100, // For very small reads
27
+ DEEP_OFFSET_THRESHOLD: 1000, // For byte estimation
28
+ SAMPLE_SIZE: 10000, // Sample size for estimation
29
+ CHUNK_SIZE: 8192, // 8KB chunks for reverse reading
30
+ };
31
+ /**
32
+ * Text file handler implementation
33
+ * Binary detection is done at the factory level - this handler assumes file is text
34
+ */
35
+ export class TextFileHandler {
36
+ canHandle(_path) {
37
+ // Text handler accepts all files that pass the factory's binary check
38
+ // The factory routes binary files to BinaryFileHandler before reaching here
39
+ return true;
40
+ }
41
+ async read(filePath, options) {
42
+ const offset = options?.offset ?? 0;
43
+ const length = options?.length ?? 1000; // Default from config
44
+ const includeStatusMessage = options?.includeStatusMessage ?? true;
45
+ // Binary detection is done at factory level - just read as text
46
+ return this.readFileWithSmartPositioning(filePath, offset, length, 'text/plain', includeStatusMessage);
47
+ }
48
+ async write(path, content, mode = 'rewrite') {
49
+ if (mode === 'append') {
50
+ await fs.appendFile(path, content);
51
+ }
52
+ else {
53
+ await fs.writeFile(path, content);
54
+ }
55
+ }
56
+ async getInfo(path) {
57
+ const stats = await fs.stat(path);
58
+ const info = {
59
+ size: stats.size,
60
+ created: stats.birthtime,
61
+ modified: stats.mtime,
62
+ accessed: stats.atime,
63
+ isDirectory: stats.isDirectory(),
64
+ isFile: stats.isFile(),
65
+ permissions: stats.mode.toString(8).slice(-3),
66
+ fileType: 'text',
67
+ metadata: {}
68
+ };
69
+ // For text files that aren't too large, count lines
70
+ if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
71
+ try {
72
+ const content = await fs.readFile(path, 'utf8');
73
+ const lineCount = TextFileHandler.countLines(content);
74
+ info.metadata.lineCount = lineCount;
75
+ }
76
+ catch (error) {
77
+ // If reading fails, skip line count
78
+ }
79
+ }
80
+ return info;
81
+ }
82
+ // ========================================================================
83
+ // Private Helper Methods (extracted from filesystem.ts)
84
+ // ========================================================================
85
+ /**
86
+ * Count lines in text content
87
+ * Made static and public for use by other modules (e.g., writeFile telemetry in filesystem.ts)
88
+ */
89
+ static countLines(content) {
90
+ return content.split('\n').length;
91
+ }
92
+ /**
93
+ * Get file line count (for files under size limit)
94
+ */
95
+ async getFileLineCount(filePath) {
96
+ try {
97
+ const stats = await fs.stat(filePath);
98
+ if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
99
+ const content = await fs.readFile(filePath, 'utf8');
100
+ return TextFileHandler.countLines(content);
101
+ }
102
+ }
103
+ catch (error) {
104
+ // If we can't read the file, return undefined
105
+ }
106
+ return undefined;
107
+ }
108
+ /**
109
+ * Generate enhanced status message
110
+ */
111
+ generateEnhancedStatusMessage(readLines, offset, totalLines, isNegativeOffset = false) {
112
+ if (isNegativeOffset) {
113
+ if (totalLines !== undefined) {
114
+ return `[Reading last ${readLines} lines (total: ${totalLines} lines)]`;
115
+ }
116
+ else {
117
+ return `[Reading last ${readLines} lines]`;
118
+ }
119
+ }
120
+ else {
121
+ if (totalLines !== undefined) {
122
+ const endLine = offset + readLines;
123
+ const remainingLines = Math.max(0, totalLines - endLine);
124
+ if (offset === 0) {
125
+ return `[Reading ${readLines} lines from start (total: ${totalLines} lines, ${remainingLines} remaining)]`;
126
+ }
127
+ else {
128
+ return `[Reading ${readLines} lines from line ${offset} (total: ${totalLines} lines, ${remainingLines} remaining)]`;
129
+ }
130
+ }
131
+ else {
132
+ if (offset === 0) {
133
+ return `[Reading ${readLines} lines from start]`;
134
+ }
135
+ else {
136
+ return `[Reading ${readLines} lines from line ${offset}]`;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Split text into lines while preserving line endings
143
+ * Made static and public for use by other modules (e.g., readFileInternal in filesystem.ts)
144
+ */
145
+ static splitLinesPreservingEndings(content) {
146
+ if (!content)
147
+ return [''];
148
+ const lines = [];
149
+ let currentLine = '';
150
+ for (let i = 0; i < content.length; i++) {
151
+ const char = content[i];
152
+ currentLine += char;
153
+ if (char === '\n') {
154
+ lines.push(currentLine);
155
+ currentLine = '';
156
+ }
157
+ else if (char === '\r') {
158
+ if (i + 1 < content.length && content[i + 1] === '\n') {
159
+ currentLine += content[i + 1];
160
+ i++;
161
+ }
162
+ lines.push(currentLine);
163
+ currentLine = '';
164
+ }
165
+ }
166
+ if (currentLine) {
167
+ lines.push(currentLine);
168
+ }
169
+ return lines;
170
+ }
171
+ /**
172
+ * Read file with smart positioning for optimal performance
173
+ */
174
+ async readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
175
+ const stats = await fs.stat(filePath);
176
+ const fileSize = stats.size;
177
+ const totalLines = await this.getFileLineCount(filePath);
178
+ // For negative offsets (tail behavior), use reverse reading
179
+ if (offset < 0) {
180
+ const requestedLines = Math.abs(offset);
181
+ if (fileSize > FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD &&
182
+ requestedLines <= READ_PERFORMANCE_THRESHOLDS.SMALL_READ_THRESHOLD) {
183
+ return await this.readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
184
+ }
185
+ else {
186
+ return await this.readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines);
187
+ }
188
+ }
189
+ // For positive offsets
190
+ else {
191
+ if (fileSize < FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD || offset === 0) {
192
+ return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
193
+ }
194
+ else {
195
+ if (offset > READ_PERFORMANCE_THRESHOLDS.DEEP_OFFSET_THRESHOLD) {
196
+ return await this.readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
197
+ }
198
+ else {
199
+ return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * Read last N lines efficiently by reading file backwards
206
+ */
207
+ async readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines) {
208
+ const fd = await fs.open(filePath, 'r');
209
+ try {
210
+ const stats = await fd.stat();
211
+ const fileSize = stats.size;
212
+ let position = fileSize;
213
+ let lines = [];
214
+ let partialLine = '';
215
+ while (position > 0 && lines.length < n) {
216
+ const readSize = Math.min(READ_PERFORMANCE_THRESHOLDS.CHUNK_SIZE, position);
217
+ position -= readSize;
218
+ const buffer = Buffer.alloc(readSize);
219
+ await fd.read(buffer, 0, readSize, position);
220
+ const chunk = buffer.toString('utf-8');
221
+ const text = chunk + partialLine;
222
+ const chunkLines = text.split('\n');
223
+ partialLine = chunkLines.shift() || '';
224
+ lines = chunkLines.concat(lines);
225
+ }
226
+ if (position === 0 && partialLine) {
227
+ lines.unshift(partialLine);
228
+ }
229
+ const result = lines.slice(-n);
230
+ const content = includeStatusMessage
231
+ ? `${this.generateEnhancedStatusMessage(result.length, -n, fileTotalLines, true)}\n\n${result.join('\n')}`
232
+ : result.join('\n');
233
+ return { content, mimeType, metadata: {} };
234
+ }
235
+ finally {
236
+ await fd.close();
237
+ }
238
+ }
239
+ /**
240
+ * Read from end using readline with circular buffer
241
+ */
242
+ async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines) {
243
+ const rl = createInterface({
244
+ input: createReadStream(filePath),
245
+ crlfDelay: Infinity
246
+ });
247
+ const buffer = new Array(requestedLines);
248
+ let bufferIndex = 0;
249
+ let totalLines = 0;
250
+ for await (const line of rl) {
251
+ buffer[bufferIndex] = line;
252
+ bufferIndex = (bufferIndex + 1) % requestedLines;
253
+ totalLines++;
254
+ }
255
+ rl.close();
256
+ let result;
257
+ if (totalLines >= requestedLines) {
258
+ result = [
259
+ ...buffer.slice(bufferIndex),
260
+ ...buffer.slice(0, bufferIndex)
261
+ ].filter(line => line !== undefined);
262
+ }
263
+ else {
264
+ result = buffer.slice(0, totalLines);
265
+ }
266
+ const content = includeStatusMessage
267
+ ? `${this.generateEnhancedStatusMessage(result.length, -requestedLines, fileTotalLines, true)}\n\n${result.join('\n')}`
268
+ : result.join('\n');
269
+ return { content, mimeType, metadata: {} };
270
+ }
271
+ /**
272
+ * Read from start/middle using readline
273
+ */
274
+ async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
275
+ const rl = createInterface({
276
+ input: createReadStream(filePath),
277
+ crlfDelay: Infinity
278
+ });
279
+ const result = [];
280
+ let lineNumber = 0;
281
+ for await (const line of rl) {
282
+ if (lineNumber >= offset && result.length < length) {
283
+ result.push(line);
284
+ }
285
+ if (result.length >= length)
286
+ break;
287
+ lineNumber++;
288
+ }
289
+ rl.close();
290
+ if (includeStatusMessage) {
291
+ const statusMessage = this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
292
+ const content = `${statusMessage}\n\n${result.join('\n')}`;
293
+ return { content, mimeType, metadata: {} };
294
+ }
295
+ else {
296
+ const content = result.join('\n');
297
+ return { content, mimeType, metadata: {} };
298
+ }
299
+ }
300
+ /**
301
+ * Read from estimated byte position for very large files
302
+ */
303
+ async readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines) {
304
+ // First, do a quick scan to estimate lines per byte
305
+ const rl = createInterface({
306
+ input: createReadStream(filePath),
307
+ crlfDelay: Infinity
308
+ });
309
+ let sampleLines = 0;
310
+ let bytesRead = 0;
311
+ for await (const line of rl) {
312
+ bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
313
+ sampleLines++;
314
+ if (bytesRead >= READ_PERFORMANCE_THRESHOLDS.SAMPLE_SIZE)
315
+ break;
316
+ }
317
+ rl.close();
318
+ if (sampleLines === 0) {
319
+ return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines);
320
+ }
321
+ // Estimate position
322
+ const avgLineLength = bytesRead / sampleLines;
323
+ const estimatedBytePosition = Math.floor(offset * avgLineLength);
324
+ const fd = await fs.open(filePath, 'r');
325
+ try {
326
+ const stats = await fd.stat();
327
+ const startPosition = Math.min(estimatedBytePosition, stats.size);
328
+ const stream = createReadStream(filePath, { start: startPosition });
329
+ const rl2 = createInterface({
330
+ input: stream,
331
+ crlfDelay: Infinity
332
+ });
333
+ const result = [];
334
+ let firstLineSkipped = false;
335
+ for await (const line of rl2) {
336
+ if (!firstLineSkipped && startPosition > 0) {
337
+ firstLineSkipped = true;
338
+ continue;
339
+ }
340
+ if (result.length < length) {
341
+ result.push(line);
342
+ }
343
+ else {
344
+ break;
345
+ }
346
+ }
347
+ rl2.close();
348
+ const content = includeStatusMessage
349
+ ? `${this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false)}\n\n${result.join('\n')}`
350
+ : result.join('\n');
351
+ return { content, mimeType, metadata: {} };
352
+ }
353
+ finally {
354
+ await fd.close();
355
+ }
356
+ }
357
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Open a URL in the default browser (cross-platform)
3
+ * Uses execFile/spawn with args array to avoid shell injection
4
+ */
5
+ export declare function openBrowser(url: string): Promise<void>;
6
+ /**
7
+ * Open the Desktop Commander welcome page
8
+ */
9
+ export declare function openWelcomePage(): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import os from 'os';
3
+ import { logToStderr } from './logger.js';
4
+ /**
5
+ * Open a URL in the default browser (cross-platform)
6
+ * Uses execFile/spawn with args array to avoid shell injection
7
+ */
8
+ export async function openBrowser(url) {
9
+ const platform = os.platform();
10
+ return new Promise((resolve, reject) => {
11
+ const callback = (error) => {
12
+ if (error) {
13
+ logToStderr('error', `Failed to open browser: ${error.message}`);
14
+ reject(error);
15
+ }
16
+ else {
17
+ logToStderr('info', `Opened browser to: ${url}`);
18
+ resolve();
19
+ }
20
+ };
21
+ switch (platform) {
22
+ case 'darwin':
23
+ execFile('open', [url], callback);
24
+ break;
25
+ case 'win32':
26
+ // Windows 'start' is a shell builtin, use spawn with shell but pass URL as separate arg
27
+ spawn('cmd', ['/c', 'start', '', url], { shell: false }).on('close', (code) => {
28
+ code === 0 ? resolve() : reject(new Error(`Exit code ${code}`));
29
+ });
30
+ break;
31
+ default:
32
+ execFile('xdg-open', [url], callback);
33
+ break;
34
+ }
35
+ });
36
+ }
37
+ /**
38
+ * Open the Desktop Commander welcome page
39
+ */
40
+ export async function openWelcomePage() {
41
+ const url = 'https://desktopcommander.app/welcome/';
42
+ await openBrowser(url);
43
+ }
@@ -31,10 +31,11 @@ export async function getRipgrepPath() {
31
31
  catch (e) {
32
32
  // @vscode/ripgrep import or binary resolution failed, continue to fallbacks
33
33
  }
34
- // Strategy 2: Try system ripgrep using 'which' command
34
+ // Strategy 2: Try system ripgrep using 'which' (Unix) or 'where' (Windows)
35
35
  try {
36
36
  const systemRg = process.platform === 'win32' ? 'rg.exe' : 'rg';
37
- const result = execSync(`which ${systemRg}`, { encoding: 'utf-8' }).trim();
37
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
38
+ const result = execSync(`${whichCmd} ${systemRg}`, { encoding: 'utf-8' }).trim().split(/\r?\n/)[0];
38
39
  if (result && existsSync(result)) {
39
40
  cachedRgPath = result;
40
41
  return result;
@@ -35,6 +35,11 @@ export interface SystemInfo {
35
35
  path: string;
36
36
  npmVersion?: string;
37
37
  };
38
+ pythonInfo?: {
39
+ available: boolean;
40
+ command: string;
41
+ version?: string;
42
+ };
38
43
  processInfo: {
39
44
  pid: number;
40
45
  arch: string;
@@ -1,6 +1,7 @@
1
1
  import os from 'os';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import { execSync } from 'child_process';
4
5
  /**
5
6
  * Detect container environment and type
6
7
  */
@@ -114,17 +115,51 @@ function discoverContainerMounts(isContainer) {
114
115
  try {
115
116
  const mountsContent = fs.readFileSync('/proc/mounts', 'utf8');
116
117
  const mountLines = mountsContent.split('\n');
118
+ // System filesystem types that are never user mounts
119
+ const systemFsTypes = new Set([
120
+ 'overlay', 'tmpfs', 'proc', 'sysfs', 'devpts', 'cgroup', 'cgroup2',
121
+ 'mqueue', 'debugfs', 'securityfs', 'pstore', 'configfs', 'fusectl',
122
+ 'hugetlbfs', 'autofs', 'devtmpfs', 'bpf', 'tracefs', 'shm'
123
+ ]);
124
+ // Filesystem types that indicate host mounts
125
+ const hostMountFsTypes = new Set(['fakeowner', '9p', 'virtiofs', 'fuse.sshfs']);
117
126
  for (const line of mountLines) {
118
127
  const parts = line.split(' ');
119
128
  if (parts.length >= 4) {
120
129
  const device = parts[0];
121
130
  const mountPoint = parts[1];
131
+ const fsType = parts[2];
122
132
  const options = parts[3];
123
- // Look for user mounts (skip system mounts)
124
- if (mountPoint.startsWith('/mnt/') ||
133
+ // Skip system mount points
134
+ const isSystemMountPoint = mountPoint === '/' ||
135
+ mountPoint.startsWith('/dev') ||
136
+ mountPoint.startsWith('/sys') ||
137
+ mountPoint.startsWith('/proc') ||
138
+ mountPoint.startsWith('/run') ||
139
+ mountPoint.startsWith('/sbin') ||
140
+ mountPoint === '/etc/resolv.conf' ||
141
+ mountPoint === '/etc/hostname' ||
142
+ mountPoint === '/etc/hosts';
143
+ if (isSystemMountPoint) {
144
+ continue;
145
+ }
146
+ // Detect user mounts by:
147
+ // 1. Known host-mount filesystem types (fakeowner, 9p, virtiofs)
148
+ // 2. Device from /run/host_mark/ (docker-mcp-gateway pattern)
149
+ // 3. Non-system filesystem type with user-like mount point
150
+ const isHostMountFs = hostMountFsTypes.has(fsType);
151
+ const isHostMarkDevice = device.startsWith('/run/host_mark/');
152
+ const isNonSystemFs = !systemFsTypes.has(fsType);
153
+ const isUserLikePath = mountPoint.startsWith('/mnt/') ||
125
154
  mountPoint.startsWith('/workspace') ||
126
155
  mountPoint.startsWith('/data/') ||
127
- (mountPoint.startsWith('/home/') && !mountPoint.startsWith('/home/root'))) {
156
+ mountPoint.startsWith('/home/') ||
157
+ mountPoint.startsWith('/Users/') ||
158
+ mountPoint.startsWith('/app/') ||
159
+ mountPoint.startsWith('/project/') ||
160
+ mountPoint.startsWith('/src/') ||
161
+ mountPoint.startsWith('/code/');
162
+ if (isHostMountFs || isHostMarkDevice || (isNonSystemFs && isUserLikePath)) {
128
163
  const isReadOnly = options.includes('ro');
129
164
  mounts.push({
130
165
  hostPath: device,
@@ -326,6 +361,36 @@ function detectNodeInfo() {
326
361
  return undefined;
327
362
  }
328
363
  }
364
+ /**
365
+ * Detect Python installation and version and put on systeminfo.pythonInfo
366
+ */
367
+ function detectPythonInfo() {
368
+ // Try python commands in order of preference
369
+ const pythonCommands = process.platform === 'win32'
370
+ ? ['python', 'python3', 'py'] // Windows: 'python' is common, 'py' launcher
371
+ : ['python3', 'python']; // Unix: prefer python3
372
+ for (const cmd of pythonCommands) {
373
+ try {
374
+ const version = execSync(`${cmd} --version`, {
375
+ encoding: 'utf8',
376
+ timeout: 5000,
377
+ stdio: ['pipe', 'pipe', 'pipe']
378
+ }).trim();
379
+ // Verify it's Python 3.x
380
+ if (version.includes('Python 3')) {
381
+ return {
382
+ available: true,
383
+ command: cmd,
384
+ version: version.replace('Python ', '')
385
+ };
386
+ }
387
+ }
388
+ catch {
389
+ // Command not found or failed, try next
390
+ }
391
+ }
392
+ return { available: false, command: '' };
393
+ }
329
394
  /**
330
395
  * Get comprehensive system information for tool prompts
331
396
  */
@@ -420,6 +485,8 @@ export function getSystemInfo() {
420
485
  }
421
486
  // Detect Node.js installation from current process
422
487
  const nodeInfo = detectNodeInfo();
488
+ // Detect Python installation
489
+ const pythonInfo = detectPythonInfo();
423
490
  // Get process information
424
491
  const processInfo = {
425
492
  pid: process.pid,
@@ -447,6 +514,7 @@ export function getSystemInfo() {
447
514
  },
448
515
  isDXT: !!process.env.MCP_DXT,
449
516
  nodeInfo,
517
+ pythonInfo,
450
518
  processInfo,
451
519
  examplePaths
452
520
  };
@@ -325,6 +325,12 @@ class UsageTracker {
325
325
  * Check if user should see onboarding invitation - SIMPLE VERSION
326
326
  */
327
327
  async shouldShowOnboarding() {
328
+ // Check feature flag first (remote kill switch)
329
+ const { featureFlagManager } = await import('./feature-flags.js');
330
+ const onboardingEnabled = featureFlagManager.get('onboarding_injection', true);
331
+ if (!onboardingEnabled) {
332
+ return false;
333
+ }
328
334
  // Check if onboarding is disabled via command line argument
329
335
  if (global.disableOnboarding) {
330
336
  return false;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Handle welcome page display for new users (A/B test controlled)
3
+ *
4
+ * Only shows to:
5
+ * 1. New users (first run - config was just created)
6
+ * 2. Users in the 'showOnboardingPage' A/B variant
7
+ * 3. Haven't seen it yet
8
+ */
9
+ export declare function handleWelcomePageOnboarding(): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { configManager } from '../config-manager.js';
2
+ import { hasFeature } from './ab-test.js';
3
+ import { openWelcomePage } from './open-browser.js';
4
+ import { logToStderr } from './logger.js';
5
+ /**
6
+ * Handle welcome page display for new users (A/B test controlled)
7
+ *
8
+ * Only shows to:
9
+ * 1. New users (first run - config was just created)
10
+ * 2. Users in the 'showOnboardingPage' A/B variant
11
+ * 3. Haven't seen it yet
12
+ */
13
+ export async function handleWelcomePageOnboarding() {
14
+ // Only for brand new users (config just created)
15
+ if (!configManager.isFirstRun()) {
16
+ return;
17
+ }
18
+ // Check A/B test assignment
19
+ const shouldShow = await hasFeature('showOnboardingPage');
20
+ if (!shouldShow) {
21
+ logToStderr('debug', 'Welcome page skipped (A/B: noOnboardingPage)');
22
+ return;
23
+ }
24
+ // Double-check not already shown (safety)
25
+ const alreadyShown = await configManager.getValue('sawOnboardingPage');
26
+ if (alreadyShown) {
27
+ return;
28
+ }
29
+ try {
30
+ await openWelcomePage();
31
+ await configManager.setValue('sawOnboardingPage', true);
32
+ logToStderr('info', 'Welcome page opened');
33
+ }
34
+ catch (e) {
35
+ logToStderr('warning', `Failed to open welcome page: ${e instanceof Error ? e.message : e}`);
36
+ }
37
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.23";
1
+ export declare const VERSION = "0.2.25";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.23';
1
+ export const VERSION = '0.2.25';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "mcpName": "io.github.wonderwhy-er/desktop-commander",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  "testemonials"
23
23
  ],
24
24
  "scripts": {
25
- "postinstall": "node dist/track-installation.js && node dist/npm-scripts/verify-ripgrep.js || true",
25
+ "postinstall": "node dist/track-installation.js && node dist/npm-scripts/verify-ripgrep.js || node -e \"process.exit(0)\"",
26
26
  "open-chat": "open -n /Applications/Claude.app",
27
27
  "sync-version": "node scripts/sync-version.js",
28
28
  "bump": "node scripts/sync-version.js --bump",
@@ -32,7 +32,7 @@
32
32
  "watch": "tsc --watch",
33
33
  "start": "node dist/index.js",
34
34
  "start:debug": "node --inspect-brk=9229 dist/index.js",
35
- "setup": "npm install && npm run build && node setup-claude-server.js",
35
+ "setup": "npm install --include=dev && npm run build && node setup-claude-server.js",
36
36
  "setup:debug": "npm install && npm run build && node setup-claude-server.js --debug",
37
37
  "remove": "npm install && npm run build && node uninstall-claude-server.js",
38
38
  "prepare": "npm run build",
@@ -78,11 +78,22 @@
78
78
  ],
79
79
  "dependencies": {
80
80
  "@modelcontextprotocol/sdk": "^1.9.0",
81
+ "@opendocsg/pdf2md": "^0.2.2",
81
82
  "@vscode/ripgrep": "^1.15.9",
82
83
  "cross-fetch": "^4.1.0",
84
+ "exceljs": "^4.4.0",
83
85
  "fastest-levenshtein": "^1.0.16",
86
+ "file-type": "^21.1.1",
84
87
  "glob": "^10.3.10",
85
88
  "isbinaryfile": "^5.0.4",
89
+ "md-to-pdf": "^5.2.5",
90
+ "pdf-lib": "^1.17.1",
91
+ "remark": "^15.0.1",
92
+ "remark-gfm": "^4.0.1",
93
+ "remark-parse": "^11.0.0",
94
+ "sharp": "^0.34.5",
95
+ "unified": "^11.0.5",
96
+ "unpdf": "^1.4.0",
86
97
  "zod": "^3.24.1",
87
98
  "zod-to-json-schema": "^3.23.5"
88
99
  },