@wonderwhy-er/desktop-commander 0.2.22 → 0.2.24

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 (60) hide show
  1. package/README.md +14 -55
  2. package/dist/custom-stdio.d.ts +1 -0
  3. package/dist/custom-stdio.js +19 -0
  4. package/dist/handlers/filesystem-handlers.d.ts +4 -0
  5. package/dist/handlers/filesystem-handlers.js +120 -14
  6. package/dist/handlers/node-handlers.d.ts +6 -0
  7. package/dist/handlers/node-handlers.js +73 -0
  8. package/dist/index.js +5 -3
  9. package/dist/search-manager.d.ts +25 -0
  10. package/dist/search-manager.js +212 -0
  11. package/dist/server.js +161 -107
  12. package/dist/terminal-manager.d.ts +56 -2
  13. package/dist/terminal-manager.js +169 -13
  14. package/dist/tools/edit.d.ts +28 -4
  15. package/dist/tools/edit.js +87 -4
  16. package/dist/tools/filesystem.d.ts +23 -12
  17. package/dist/tools/filesystem.js +201 -416
  18. package/dist/tools/improved-process-tools.d.ts +2 -2
  19. package/dist/tools/improved-process-tools.js +244 -214
  20. package/dist/tools/mime-types.d.ts +1 -0
  21. package/dist/tools/mime-types.js +7 -0
  22. package/dist/tools/pdf/extract-images.d.ts +34 -0
  23. package/dist/tools/pdf/extract-images.js +132 -0
  24. package/dist/tools/pdf/index.d.ts +6 -0
  25. package/dist/tools/pdf/index.js +3 -0
  26. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  27. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  28. package/dist/tools/pdf/manipulations.d.ts +13 -0
  29. package/dist/tools/pdf/manipulations.js +96 -0
  30. package/dist/tools/pdf/markdown.d.ts +7 -0
  31. package/dist/tools/pdf/markdown.js +37 -0
  32. package/dist/tools/pdf/utils.d.ts +12 -0
  33. package/dist/tools/pdf/utils.js +34 -0
  34. package/dist/tools/prompts.js +0 -10
  35. package/dist/tools/schemas.d.ts +167 -12
  36. package/dist/tools/schemas.js +54 -5
  37. package/dist/types.d.ts +2 -1
  38. package/dist/utils/feature-flags.js +7 -4
  39. package/dist/utils/files/base.d.ts +167 -0
  40. package/dist/utils/files/base.js +5 -0
  41. package/dist/utils/files/binary.d.ts +21 -0
  42. package/dist/utils/files/binary.js +65 -0
  43. package/dist/utils/files/excel.d.ts +24 -0
  44. package/dist/utils/files/excel.js +416 -0
  45. package/dist/utils/files/factory.d.ts +40 -0
  46. package/dist/utils/files/factory.js +101 -0
  47. package/dist/utils/files/image.d.ts +21 -0
  48. package/dist/utils/files/image.js +78 -0
  49. package/dist/utils/files/index.d.ts +10 -0
  50. package/dist/utils/files/index.js +13 -0
  51. package/dist/utils/files/pdf.d.ts +32 -0
  52. package/dist/utils/files/pdf.js +142 -0
  53. package/dist/utils/files/text.d.ts +63 -0
  54. package/dist/utils/files/text.js +357 -0
  55. package/dist/utils/ripgrep-resolver.js +3 -2
  56. package/dist/utils/system-info.d.ts +5 -0
  57. package/dist/utils/system-info.js +71 -3
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +14 -3
@@ -156,7 +156,8 @@ export class TerminalManager {
156
156
  const session = {
157
157
  pid: childProcess.pid,
158
158
  process: childProcess,
159
- lastOutput: '',
159
+ outputLines: [], // Line-based buffer
160
+ lastReadIndex: 0, // Track where "new" output starts
160
161
  isBlocked: false,
161
162
  startTime: new Date()
162
163
  };
@@ -201,7 +202,8 @@ export class TerminalManager {
201
202
  firstOutputTime = now;
202
203
  lastOutputTime = now;
203
204
  output += text;
204
- session.lastOutput += text;
205
+ // Append to line-based buffer
206
+ this.appendToLineBuffer(session, text);
205
207
  // Record output event if collecting timing
206
208
  if (collectTiming) {
207
209
  outputEvents.push({
@@ -233,7 +235,8 @@ export class TerminalManager {
233
235
  firstOutputTime = now;
234
236
  lastOutputTime = now;
235
237
  output += text;
236
- session.lastOutput += text;
238
+ // Append to line-based buffer
239
+ this.appendToLineBuffer(session, text);
237
240
  // Record output event if collecting timing
238
241
  if (collectTiming) {
239
242
  outputEvents.push({
@@ -275,7 +278,7 @@ export class TerminalManager {
275
278
  // Store completed session before removing active session
276
279
  this.completedSessions.set(childProcess.pid, {
277
280
  pid: childProcess.pid,
278
- output: output, // Use only the main output variable
281
+ outputLines: [...session.outputLines], // Copy line buffer
279
282
  exitCode: code,
280
283
  startTime: session.startTime,
281
284
  endTime: new Date()
@@ -296,26 +299,179 @@ export class TerminalManager {
296
299
  });
297
300
  });
298
301
  }
299
- getNewOutput(pid) {
302
+ /**
303
+ * Append text to a session's line buffer
304
+ * Handles partial lines and newline splitting
305
+ */
306
+ appendToLineBuffer(session, text) {
307
+ if (!text)
308
+ return;
309
+ // Split text into lines, keeping track of whether text ends with newline
310
+ const lines = text.split('\n');
311
+ for (let i = 0; i < lines.length; i++) {
312
+ const line = lines[i];
313
+ const isLastFragment = i === lines.length - 1;
314
+ const endsWithNewline = text.endsWith('\n');
315
+ if (session.outputLines.length === 0) {
316
+ // First line ever
317
+ session.outputLines.push(line);
318
+ }
319
+ else if (i === 0) {
320
+ // First fragment - append to last line (might be partial)
321
+ session.outputLines[session.outputLines.length - 1] += line;
322
+ }
323
+ else {
324
+ // Subsequent lines - add as new lines
325
+ session.outputLines.push(line);
326
+ }
327
+ }
328
+ }
329
+ /**
330
+ * Read process output with pagination (like file reading)
331
+ * @param pid Process ID
332
+ * @param offset Line offset: 0=from lastReadIndex, positive=absolute, negative=tail
333
+ * @param length Max lines to return
334
+ * @param updateReadIndex Whether to update lastReadIndex (default: true for offset=0)
335
+ */
336
+ readOutputPaginated(pid, offset = 0, length = 1000) {
300
337
  // First check active sessions
301
338
  const session = this.sessions.get(pid);
302
339
  if (session) {
303
- const output = session.lastOutput;
304
- session.lastOutput = '';
305
- return output;
340
+ return this.readFromLineBuffer(session.outputLines, offset, length, session.lastReadIndex, (newIndex) => { session.lastReadIndex = newIndex; }, false, undefined);
306
341
  }
307
342
  // Then check completed sessions
308
343
  const completedSession = this.completedSessions.get(pid);
309
344
  if (completedSession) {
310
- // Format with output first, then completion info
311
- const runtime = (completedSession.endTime.getTime() - completedSession.startTime.getTime()) / 1000;
312
- const output = completedSession.output.trim();
345
+ const runtimeMs = completedSession.endTime.getTime() - completedSession.startTime.getTime();
346
+ return this.readFromLineBuffer(completedSession.outputLines, offset, length, 0, // Completed sessions don't track read position
347
+ () => { }, // No-op for completed sessions
348
+ true, completedSession.exitCode, runtimeMs);
349
+ }
350
+ return null;
351
+ }
352
+ /**
353
+ * Internal helper to read from a line buffer with offset/length
354
+ */
355
+ readFromLineBuffer(lines, offset, length, lastReadIndex, updateLastRead, isComplete, exitCode, runtimeMs) {
356
+ const totalLines = lines.length;
357
+ let startIndex;
358
+ let linesToRead;
359
+ if (offset < 0) {
360
+ // Negative offset = start position from end, then read 'length' lines forward
361
+ // e.g., offset=-50, length=10 means: start 50 lines from end, read 10 lines
362
+ const fromEnd = Math.abs(offset);
363
+ startIndex = Math.max(0, totalLines - fromEnd);
364
+ linesToRead = lines.slice(startIndex, startIndex + length);
365
+ // Don't update lastReadIndex for tail reads
366
+ }
367
+ else if (offset === 0) {
368
+ // offset=0 means "from where I last read" (like getNewOutput)
369
+ startIndex = lastReadIndex;
370
+ linesToRead = lines.slice(startIndex, startIndex + length);
371
+ // Update lastReadIndex for "new output" behavior
372
+ updateLastRead(Math.min(startIndex + linesToRead.length, totalLines));
373
+ }
374
+ else {
375
+ // Positive offset = absolute position
376
+ startIndex = offset;
377
+ linesToRead = lines.slice(startIndex, startIndex + length);
378
+ // Don't update lastReadIndex for absolute position reads
379
+ }
380
+ const readCount = linesToRead.length;
381
+ const endIndex = startIndex + readCount;
382
+ const remaining = Math.max(0, totalLines - endIndex);
383
+ return {
384
+ lines: linesToRead,
385
+ totalLines,
386
+ readFrom: startIndex,
387
+ readCount,
388
+ remaining,
389
+ isComplete,
390
+ exitCode,
391
+ runtimeMs
392
+ };
393
+ }
394
+ /**
395
+ * Get total line count for a process
396
+ */
397
+ getOutputLineCount(pid) {
398
+ const session = this.sessions.get(pid);
399
+ if (session) {
400
+ return session.outputLines.length;
401
+ }
402
+ const completedSession = this.completedSessions.get(pid);
403
+ if (completedSession) {
404
+ return completedSession.outputLines.length;
405
+ }
406
+ return null;
407
+ }
408
+ /**
409
+ * Legacy method for backward compatibility
410
+ * Returns all new output since last read
411
+ * @param maxLines Maximum lines to return (default: 1000 for context protection)
412
+ * @deprecated Use readOutputPaginated instead
413
+ */
414
+ getNewOutput(pid, maxLines = 1000) {
415
+ const result = this.readOutputPaginated(pid, 0, maxLines);
416
+ if (!result)
417
+ return null;
418
+ const output = result.lines.join('\n').trim();
419
+ // For completed sessions, append completion info with runtime
420
+ if (result.isComplete) {
421
+ const runtimeStr = result.runtimeMs !== undefined
422
+ ? `\nRuntime: ${(result.runtimeMs / 1000).toFixed(2)}s`
423
+ : '';
313
424
  if (output) {
314
- return `${output}\n\nProcess completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s`;
425
+ return `${output}\n\nProcess completed with exit code ${result.exitCode}${runtimeStr}`;
315
426
  }
316
427
  else {
317
- return `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s\n(No output produced)`;
428
+ return `Process completed with exit code ${result.exitCode}${runtimeStr}\n(No output produced)`;
429
+ }
430
+ }
431
+ // Add truncation warning if there's more output
432
+ if (result.remaining > 0) {
433
+ return `${output}\n\n[Output truncated: ${result.remaining} more lines available. Use read_process_output with offset/length for full output.]`;
434
+ }
435
+ return output || null;
436
+ }
437
+ /**
438
+ * Capture a snapshot of current output state for interaction tracking.
439
+ * Used by interactWithProcess to know what output existed before sending input.
440
+ */
441
+ captureOutputSnapshot(pid) {
442
+ const session = this.sessions.get(pid);
443
+ if (session) {
444
+ const fullOutput = session.outputLines.join('\n');
445
+ return {
446
+ totalChars: fullOutput.length,
447
+ lineCount: session.outputLines.length
448
+ };
449
+ }
450
+ return null;
451
+ }
452
+ /**
453
+ * Get output that appeared since a snapshot was taken.
454
+ * This handles the case where output is appended to the last line (REPL prompts).
455
+ * Also checks completed sessions in case process finished between snapshot and poll.
456
+ */
457
+ getOutputSinceSnapshot(pid, snapshot) {
458
+ // Check active session first
459
+ const session = this.sessions.get(pid);
460
+ if (session) {
461
+ const fullOutput = session.outputLines.join('\n');
462
+ if (fullOutput.length <= snapshot.totalChars) {
463
+ return ''; // No new output
464
+ }
465
+ return fullOutput.substring(snapshot.totalChars);
466
+ }
467
+ // Fallback to completed sessions - process may have finished between snapshot and poll
468
+ const completedSession = this.completedSessions.get(pid);
469
+ if (completedSession) {
470
+ const fullOutput = completedSession.outputLines.join('\n');
471
+ if (fullOutput.length <= snapshot.totalChars) {
472
+ return ''; // No new output
318
473
  }
474
+ return fullOutput.substring(snapshot.totalChars);
319
475
  }
320
476
  return null;
321
477
  }
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Text file editing via search/replace with fuzzy matching support.
3
+ *
4
+ * TECHNICAL DEBT / ARCHITECTURAL NOTE:
5
+ * This file contains text editing logic that should ideally live in TextFileHandler.editRange()
6
+ * to be consistent with how Excel editing works (ExcelFileHandler.editRange()).
7
+ *
8
+ * Current inconsistency:
9
+ * - Excel: edit_block → ExcelFileHandler.editRange() ✓ uses file handler
10
+ * - Text: edit_block → performSearchReplace() here → bypasses TextFileHandler
11
+ *
12
+ * Future refactor should:
13
+ * 1. Move performSearchReplace() + fuzzy logic into TextFileHandler.editRange()
14
+ * 2. Make this file a thin dispatch layer that routes to appropriate FileHandler
15
+ * 3. Unify the editRange() signature to handle both text search/replace and structured edits
16
+ */
1
17
  import { ServerResult } from '../types.js';
2
18
  interface SearchReplace {
3
19
  search: string;
@@ -5,10 +21,18 @@ interface SearchReplace {
5
21
  }
6
22
  export declare function performSearchReplace(filePath: string, block: SearchReplace, expectedReplacements?: number): Promise<ServerResult>;
7
23
  /**
8
- * Handle edit_block command with enhanced functionality
9
- * - Supports multiple replacements
10
- * - Validates expected replacements count
11
- * - Provides detailed error messages
24
+ * Handle edit_block command
25
+ *
26
+ * 1. Text files: String replacement (old_string/new_string)
27
+ * - Uses fuzzy matching for resilience
28
+ * - Handles expected_replacements parameter
29
+ *
30
+ * 2. Structured files (Excel): Range rewrite (range + content)
31
+ * - Bulk updates to cell ranges (e.g., "Sheet1!A1:C10")
32
+ * - Whole sheet replacement (e.g., "Sheet1")
33
+ * - More powerful and simpler than surgical location-based edits
34
+ * - Supports chunking for large datasets (e.g., 1000 rows at a time)
35
+
12
36
  */
13
37
  export declare function handleEditBlock(args: unknown): Promise<ServerResult>;
14
38
  export {};
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Text file editing via search/replace with fuzzy matching support.
3
+ *
4
+ * TECHNICAL DEBT / ARCHITECTURAL NOTE:
5
+ * This file contains text editing logic that should ideally live in TextFileHandler.editRange()
6
+ * to be consistent with how Excel editing works (ExcelFileHandler.editRange()).
7
+ *
8
+ * Current inconsistency:
9
+ * - Excel: edit_block → ExcelFileHandler.editRange() ✓ uses file handler
10
+ * - Text: edit_block → performSearchReplace() here → bypasses TextFileHandler
11
+ *
12
+ * Future refactor should:
13
+ * 1. Move performSearchReplace() + fuzzy logic into TextFileHandler.editRange()
14
+ * 2. Make this file a thin dispatch layer that routes to appropriate FileHandler
15
+ * 3. Unify the editRange() signature to handle both text search/replace and structured edits
16
+ */
1
17
  import { writeFile, readFileInternal, validatePath } from './filesystem.js';
2
18
  import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
3
19
  import { capture } from '../utils/capture.js';
@@ -273,13 +289,80 @@ function highlightDifferences(expected, actual) {
273
289
  return `${commonPrefix}{-${expectedDiff}-}{+${actualDiff}+}${commonSuffix}`;
274
290
  }
275
291
  /**
276
- * Handle edit_block command with enhanced functionality
277
- * - Supports multiple replacements
278
- * - Validates expected replacements count
279
- * - Provides detailed error messages
292
+ * Handle edit_block command
293
+ *
294
+ * 1. Text files: String replacement (old_string/new_string)
295
+ * - Uses fuzzy matching for resilience
296
+ * - Handles expected_replacements parameter
297
+ *
298
+ * 2. Structured files (Excel): Range rewrite (range + content)
299
+ * - Bulk updates to cell ranges (e.g., "Sheet1!A1:C10")
300
+ * - Whole sheet replacement (e.g., "Sheet1")
301
+ * - More powerful and simpler than surgical location-based edits
302
+ * - Supports chunking for large datasets (e.g., 1000 rows at a time)
303
+
280
304
  */
281
305
  export async function handleEditBlock(args) {
282
306
  const parsed = EditBlockArgsSchema.parse(args);
307
+ // Structured files: Range rewrite
308
+ if (parsed.range !== undefined && parsed.content !== undefined) {
309
+ try {
310
+ // Validate path before any filesystem operations
311
+ const validatedPath = await validatePath(parsed.file_path);
312
+ const { getFileHandler } = await import('../utils/files/factory.js');
313
+ const handler = await getFileHandler(validatedPath);
314
+ // Parse content if it's a JSON string (AI often sends arrays as JSON strings)
315
+ let content = parsed.content;
316
+ if (typeof content === 'string') {
317
+ try {
318
+ content = JSON.parse(content);
319
+ }
320
+ catch {
321
+ // Leave as-is if not valid JSON - let handler decide
322
+ }
323
+ }
324
+ // Check if handler supports range editing
325
+ if ('editRange' in handler && typeof handler.editRange === 'function') {
326
+ await handler.editRange(validatedPath, parsed.range, content, parsed.options);
327
+ return {
328
+ content: [{
329
+ type: "text",
330
+ text: `Successfully updated range ${parsed.range} in ${parsed.file_path}`
331
+ }],
332
+ };
333
+ }
334
+ else {
335
+ return {
336
+ content: [{
337
+ type: "text",
338
+ text: `Error: Range-based editing not supported for ${parsed.file_path}`
339
+ }],
340
+ isError: true
341
+ };
342
+ }
343
+ }
344
+ catch (error) {
345
+ const errorMessage = error instanceof Error ? error.message : String(error);
346
+ return {
347
+ content: [{
348
+ type: "text",
349
+ text: `Error: ${errorMessage}`
350
+ }],
351
+ isError: true
352
+ };
353
+ }
354
+ }
355
+ // Text files: String replacement
356
+ // Validate required parameters for text replacement
357
+ if (parsed.old_string === undefined || parsed.new_string === undefined) {
358
+ return {
359
+ content: [{
360
+ type: "text",
361
+ text: `Error: Text replacement requires both old_string and new_string parameters`
362
+ }],
363
+ isError: true
364
+ };
365
+ }
283
366
  const searchReplace = {
284
367
  search: parsed.old_string,
285
368
  replace: parsed.new_string
@@ -1,3 +1,5 @@
1
+ import type { ReadOptions, FileResult, PdfPageItem } from '../utils/files/base.js';
2
+ import { PdfOperations, PdfMetadata } from './pdf/index.js';
1
3
  /**
2
4
  * Validates a path to ensure it can be accessed or created.
3
5
  * For existing paths, returns the real path (resolving symlinks).
@@ -8,11 +10,12 @@
8
10
  * @throws Error if the path or its parent directories don't exist or if the path is not allowed
9
11
  */
10
12
  export declare function validatePath(requestedPath: string): Promise<string>;
11
- export interface FileResult {
12
- content: string;
13
- mimeType: string;
14
- isImage: boolean;
15
- }
13
+ export type { FileResult } from '../utils/files/base.js';
14
+ type PdfPayload = {
15
+ metadata: PdfMetadata;
16
+ pages: PdfPageItem[];
17
+ };
18
+ type FileResultPayloads = PdfPayload;
16
19
  /**
17
20
  * Read file content from a URL
18
21
  * @param url URL to fetch content from
@@ -22,20 +25,17 @@ export declare function readFileFromUrl(url: string): Promise<FileResult>;
22
25
  /**
23
26
  * Read file content from the local filesystem
24
27
  * @param filePath Path to the file
25
- * @param offset Starting line number to read from (default: 0)
26
- * @param length Maximum number of lines to read (default: from config or 1000)
28
+ * @param options Read options (offset, length, sheet, range)
27
29
  * @returns File content or file result with metadata
28
30
  */
29
- export declare function readFileFromDisk(filePath: string, offset?: number, length?: number): Promise<FileResult>;
31
+ export declare function readFileFromDisk(filePath: string, options?: ReadOptions): Promise<FileResult>;
30
32
  /**
31
33
  * Read a file from either the local filesystem or a URL
32
34
  * @param filePath Path to the file or URL
33
- * @param isUrl Whether the path is a URL
34
- * @param offset Starting line number to read from (default: 0)
35
- * @param length Maximum number of lines to read (default: from config or 1000)
35
+ * @param options Read options (isUrl, offset, length, sheet, range)
36
36
  * @returns File content or file result with metadata
37
37
  */
38
- export declare function readFile(filePath: string, isUrl?: boolean, offset?: number, length?: number): Promise<FileResult>;
38
+ export declare function readFile(filePath: string, options?: ReadOptions): Promise<FileResult>;
39
39
  /**
40
40
  * Read file content without status messages for internal operations
41
41
  * This function preserves exact file content including original line endings,
@@ -53,6 +53,8 @@ export interface MultiFileResult {
53
53
  mimeType?: string;
54
54
  isImage?: boolean;
55
55
  error?: string;
56
+ isPdf?: boolean;
57
+ payload?: FileResultPayloads;
56
58
  }
57
59
  export declare function readMultipleFiles(paths: string[]): Promise<MultiFileResult[]>;
58
60
  export declare function createDirectory(dirPath: string): Promise<void>;
@@ -60,3 +62,12 @@ export declare function listDirectory(dirPath: string, depth?: number): Promise<
60
62
  export declare function moveFile(sourcePath: string, destinationPath: string): Promise<void>;
61
63
  export declare function searchFiles(rootPath: string, pattern: string): Promise<string[]>;
62
64
  export declare function getFileInfo(filePath: string): Promise<Record<string, any>>;
65
+ /**
66
+ * Write content to a PDF file.
67
+ * Can create a new PDF from Markdown string, or modify an existing PDF using operations.
68
+ *
69
+ * @param filePath Path to the output PDF file
70
+ * @param content Markdown string (for creation) or array of operations (for modification)
71
+ * @param options Options for PDF generation or modification. For modification, can include `sourcePdf`.
72
+ */
73
+ export declare function writePdf(filePath: string, content: string | PdfOperations[], outputPath?: string, options?: any): Promise<void>;