@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.
- package/README.md +14 -55
- package/dist/config-manager.d.ts +5 -0
- package/dist/config-manager.js +9 -0
- package/dist/custom-stdio.d.ts +1 -0
- package/dist/custom-stdio.js +19 -0
- package/dist/handlers/filesystem-handlers.d.ts +4 -0
- package/dist/handlers/filesystem-handlers.js +120 -14
- package/dist/handlers/node-handlers.d.ts +6 -0
- package/dist/handlers/node-handlers.js +73 -0
- package/dist/index.js +5 -3
- package/dist/search-manager.d.ts +25 -0
- package/dist/search-manager.js +212 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +188 -73
- package/dist/terminal-manager.d.ts +56 -2
- package/dist/terminal-manager.js +169 -13
- package/dist/tools/edit.d.ts +28 -4
- package/dist/tools/edit.js +87 -4
- package/dist/tools/filesystem.d.ts +23 -12
- package/dist/tools/filesystem.js +201 -416
- package/dist/tools/improved-process-tools.d.ts +2 -2
- package/dist/tools/improved-process-tools.js +244 -214
- package/dist/tools/mime-types.d.ts +1 -0
- package/dist/tools/mime-types.js +7 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +96 -0
- package/dist/tools/pdf/markdown.d.ts +7 -0
- package/dist/tools/pdf/markdown.js +37 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/schemas.d.ts +167 -12
- package/dist/tools/schemas.js +54 -5
- package/dist/types.d.ts +2 -1
- package/dist/utils/ab-test.d.ts +8 -0
- package/dist/utils/ab-test.js +76 -0
- package/dist/utils/capture.js +5 -0
- package/dist/utils/feature-flags.js +7 -4
- package/dist/utils/files/base.d.ts +167 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +416 -0
- package/dist/utils/files/factory.d.ts +40 -0
- package/dist/utils/files/factory.js +101 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +357 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +43 -0
- package/dist/utils/ripgrep-resolver.js +3 -2
- package/dist/utils/system-info.d.ts +5 -0
- package/dist/utils/system-info.js +71 -3
- package/dist/utils/usageTracker.js +6 -0
- package/dist/utils/welcome-onboarding.d.ts +9 -0
- package/dist/utils/welcome-onboarding.js +37 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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'
|
|
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
|
|
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;
|
|
@@ -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
|
-
//
|
|
124
|
-
|
|
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
|
-
|
|
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.
|
|
1
|
+
export declare const VERSION = "0.2.25";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
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.
|
|
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 ||
|
|
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
|
},
|