@wonderwhy-er/desktop-commander 0.2.15 → 0.2.17

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.
@@ -757,10 +757,58 @@ export async function createDirectory(dirPath) {
757
757
  const validPath = await validatePath(dirPath);
758
758
  await fs.mkdir(validPath, { recursive: true });
759
759
  }
760
- export async function listDirectory(dirPath) {
760
+ export async function listDirectory(dirPath, depth = 2) {
761
761
  const validPath = await validatePath(dirPath);
762
- const entries = await fs.readdir(validPath, { withFileTypes: true });
763
- return entries.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`);
762
+ const results = [];
763
+ const MAX_NESTED_ITEMS = 100; // Maximum items to show per nested directory
764
+ async function listRecursive(currentPath, currentDepth, relativePath = '', isTopLevel = true) {
765
+ if (currentDepth <= 0)
766
+ return;
767
+ let entries;
768
+ try {
769
+ entries = await fs.readdir(currentPath, { withFileTypes: true });
770
+ }
771
+ catch (error) {
772
+ // If we can't read this directory (permission denied), show as denied
773
+ const displayPath = relativePath || path.basename(currentPath);
774
+ results.push(`[DENIED] ${displayPath}`);
775
+ return;
776
+ }
777
+ // Apply filtering for nested directories (not top level)
778
+ const totalEntries = entries.length;
779
+ let entriesToShow = entries;
780
+ let filteredCount = 0;
781
+ if (!isTopLevel && totalEntries > MAX_NESTED_ITEMS) {
782
+ entriesToShow = entries.slice(0, MAX_NESTED_ITEMS);
783
+ filteredCount = totalEntries - MAX_NESTED_ITEMS;
784
+ }
785
+ for (const entry of entriesToShow) {
786
+ const fullPath = path.join(currentPath, entry.name);
787
+ const displayPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
788
+ // Add this entry to results
789
+ results.push(`${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${displayPath}`);
790
+ // If it's a directory and we have depth remaining, recurse
791
+ if (entry.isDirectory() && currentDepth > 1) {
792
+ try {
793
+ // Validate the path before recursing
794
+ await validatePath(fullPath);
795
+ await listRecursive(fullPath, currentDepth - 1, displayPath, false);
796
+ }
797
+ catch (error) {
798
+ // If validation fails or we can't access it, it will be marked as denied
799
+ // when we try to read it in the recursive call
800
+ continue;
801
+ }
802
+ }
803
+ }
804
+ // Add warning message if items were filtered
805
+ if (filteredCount > 0) {
806
+ const displayPath = relativePath || path.basename(currentPath);
807
+ results.push(`[WARNING] ${displayPath}: ${filteredCount} items hidden (showing first ${MAX_NESTED_ITEMS} of ${totalEntries} total)`);
808
+ }
809
+ }
810
+ await listRecursive(validPath, depth, '', true);
811
+ return results;
764
812
  }
765
813
  export async function moveFile(sourcePath, destinationPath) {
766
814
  const validSourcePath = await validatePath(sourcePath);
@@ -94,10 +94,13 @@ export declare const CreateDirectoryArgsSchema: z.ZodObject<{
94
94
  }>;
95
95
  export declare const ListDirectoryArgsSchema: z.ZodObject<{
96
96
  path: z.ZodString;
97
+ depth: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
97
98
  }, "strip", z.ZodTypeAny, {
98
99
  path: string;
100
+ depth: number;
99
101
  }, {
100
102
  path: string;
103
+ depth?: number | undefined;
101
104
  }>;
102
105
  export declare const MoveFileArgsSchema: z.ZodObject<{
103
106
  source: z.ZodString;
@@ -221,3 +224,16 @@ export declare const GetPromptsArgsSchema: z.ZodObject<{
221
224
  category?: string | undefined;
222
225
  promptId?: string | undefined;
223
226
  }>;
227
+ export declare const GetRecentToolCallsArgsSchema: z.ZodObject<{
228
+ maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
229
+ toolName: z.ZodOptional<z.ZodString>;
230
+ since: z.ZodOptional<z.ZodString>;
231
+ }, "strip", z.ZodTypeAny, {
232
+ maxResults: number;
233
+ toolName?: string | undefined;
234
+ since?: string | undefined;
235
+ }, {
236
+ maxResults?: number | undefined;
237
+ toolName?: string | undefined;
238
+ since?: string | undefined;
239
+ }>;
@@ -50,6 +50,7 @@ export const CreateDirectoryArgsSchema = z.object({
50
50
  });
51
51
  export const ListDirectoryArgsSchema = z.object({
52
52
  path: z.string(),
53
+ depth: z.number().optional().default(2),
53
54
  });
54
55
  export const MoveFileArgsSchema = z.object({
55
56
  source: z.string(),
@@ -112,3 +113,9 @@ export const GetPromptsArgsSchema = z.object({
112
113
  category: z.string().optional(),
113
114
  promptId: z.string().optional(),
114
115
  });
116
+ // Tool history schema
117
+ export const GetRecentToolCallsArgsSchema = z.object({
118
+ maxResults: z.number().min(1).max(1000).optional().default(50),
119
+ toolName: z.string().optional(),
120
+ since: z.string().datetime().optional(),
121
+ });
@@ -29,6 +29,18 @@ export interface SystemInfo {
29
29
  isMacOS: boolean;
30
30
  isLinux: boolean;
31
31
  docker: ContainerInfo;
32
+ isDXT: boolean;
33
+ nodeInfo?: {
34
+ version: string;
35
+ path: string;
36
+ npmVersion?: string;
37
+ };
38
+ processInfo: {
39
+ pid: number;
40
+ arch: string;
41
+ platform: string;
42
+ versions: NodeJS.ProcessVersions;
43
+ };
32
44
  examplePaths: {
33
45
  home: string;
34
46
  temp: string;
@@ -305,6 +305,27 @@ function getContainerEnvironment(containerType) {
305
305
  }
306
306
  return Object.keys(env).length > 0 ? env : undefined;
307
307
  }
308
+ /**
309
+ * Detect Node.js installation and version from current process
310
+ */
311
+ function detectNodeInfo() {
312
+ try {
313
+ // Get Node.js version from current process
314
+ const version = process.version.replace('v', ''); // Remove 'v' prefix
315
+ // Get Node.js executable path from current process
316
+ const path = process.execPath;
317
+ // Get npm version from environment if available
318
+ const npmVersion = process.env.npm_version;
319
+ return {
320
+ version,
321
+ path,
322
+ ...(npmVersion && { npmVersion })
323
+ };
324
+ }
325
+ catch (error) {
326
+ return undefined;
327
+ }
328
+ }
308
329
  /**
309
330
  * Get comprehensive system information for tool prompts
310
331
  */
@@ -397,6 +418,15 @@ export function getSystemInfo() {
397
418
  examplePaths.accessible = mountPoints.map(mount => mount.containerPath);
398
419
  }
399
420
  }
421
+ // Detect Node.js installation from current process
422
+ const nodeInfo = detectNodeInfo();
423
+ // Get process information
424
+ const processInfo = {
425
+ pid: process.pid,
426
+ arch: process.arch,
427
+ platform: process.platform,
428
+ versions: process.versions
429
+ };
400
430
  return {
401
431
  platform,
402
432
  platformName,
@@ -415,6 +445,9 @@ export function getSystemInfo() {
415
445
  mountPoints,
416
446
  containerEnvironment: getContainerEnvironment(containerDetection.containerType)
417
447
  },
448
+ isDXT: !!process.env.MCP_DXT,
449
+ nodeInfo,
450
+ processInfo,
418
451
  examplePaths
419
452
  };
420
453
  }
@@ -545,33 +578,51 @@ LINUX-SPECIFIC NOTES:
545
578
  * Get common development tool guidance based on OS
546
579
  */
547
580
  export function getDevelopmentToolGuidance(systemInfo) {
548
- const { isWindows, isMacOS, isLinux, platformName } = systemInfo;
581
+ const { isWindows, isMacOS, isLinux, platformName, nodeInfo, processInfo } = systemInfo;
582
+ // Add detected Node.js info to guidance
583
+ const nodeGuidance = nodeInfo
584
+ ? `Node.js: v${nodeInfo.version} (${nodeInfo.path})${nodeInfo.npmVersion ? ` | npm: v${nodeInfo.npmVersion}` : ''}`
585
+ : 'Node.js: Not detected';
586
+ // Add process environment info
587
+ const envInfo = `
588
+ Current Process Environment:
589
+ - Node: v${processInfo.versions.node}
590
+ - V8: v${processInfo.versions.v8}
591
+ - Architecture: ${processInfo.arch}
592
+ - Platform: ${processInfo.platform}
593
+ - Process ID: ${processInfo.pid}`;
549
594
  if (isWindows) {
550
595
  return `
551
596
  COMMON WINDOWS DEVELOPMENT TOOLS:
552
- - Node.js: Usually installed globally, accessible from any shell
597
+ - ${nodeGuidance}
553
598
  - Python: May be 'python' or 'py' command, check both
554
599
  - Git: Git Bash provides Unix-like environment
555
600
  - WSL: Windows Subsystem for Linux available for Unix tools
556
- - Visual Studio tools: cl, msbuild for C++ compilation`;
601
+ - Visual Studio tools: cl, msbuild for C++ compilation
602
+
603
+ ${envInfo}`;
557
604
  }
558
605
  else if (isMacOS) {
559
606
  return `
560
607
  COMMON MACOS DEVELOPMENT TOOLS:
561
608
  - Xcode Command Line Tools: Required for many development tools
562
609
  - Homebrew: Primary package manager for development tools
610
+ - ${nodeGuidance}
563
611
  - Python: Usually python3, check if python points to Python 2
564
- - Node.js: Available via brew or direct installer
565
- - Ruby: System Ruby available, rbenv/rvm for version management`;
612
+ - Ruby: System Ruby available, rbenv/rvm for version management
613
+
614
+ ${envInfo}`;
566
615
  }
567
616
  else {
568
617
  return `
569
618
  COMMON LINUX DEVELOPMENT TOOLS:
570
619
  - Package managers: Install tools via distribution package manager
571
620
  - Python: Usually python3, python may point to Python 2
572
- - Node.js: Available via package manager or NodeSource repository
621
+ - ${nodeGuidance}
573
622
  - Build tools: gcc, make typically available or easily installed
574
- - Container tools: docker, podman common for development`;
623
+ - Container tools: docker, podman common for development
624
+
625
+ ${envInfo}`;
575
626
  }
576
627
  }
577
628
  /**
@@ -0,0 +1,73 @@
1
+ import { ServerResult } from '../types.js';
2
+ export interface ToolCallRecord {
3
+ timestamp: string;
4
+ toolName: string;
5
+ arguments: any;
6
+ output: ServerResult;
7
+ duration?: number;
8
+ }
9
+ interface FormattedToolCallRecord extends Omit<ToolCallRecord, 'timestamp'> {
10
+ timestamp: string;
11
+ }
12
+ declare class ToolHistory {
13
+ private history;
14
+ private readonly MAX_ENTRIES;
15
+ private readonly historyFile;
16
+ private writeQueue;
17
+ private isWriting;
18
+ private writeInterval?;
19
+ constructor();
20
+ /**
21
+ * Load history from disk (all instances share the same file)
22
+ */
23
+ private loadFromDisk;
24
+ /**
25
+ * Trim history file to prevent it from growing indefinitely
26
+ */
27
+ private trimHistoryFile;
28
+ /**
29
+ * Async write processor - batches writes to avoid blocking
30
+ */
31
+ private startWriteProcessor;
32
+ /**
33
+ * Flush queued writes to disk
34
+ */
35
+ private flushToDisk;
36
+ /**
37
+ * Add a tool call to history
38
+ */
39
+ addCall(toolName: string, args: any, output: ServerResult, duration?: number): void;
40
+ /**
41
+ * Get recent tool calls with filters
42
+ */
43
+ getRecentCalls(options: {
44
+ maxResults?: number;
45
+ toolName?: string;
46
+ since?: string;
47
+ }): ToolCallRecord[];
48
+ /**
49
+ * Get recent calls formatted with local timezone
50
+ */
51
+ getRecentCallsFormatted(options: {
52
+ maxResults?: number;
53
+ toolName?: string;
54
+ since?: string;
55
+ }): FormattedToolCallRecord[];
56
+ /**
57
+ * Get current stats
58
+ */
59
+ getStats(): {
60
+ totalEntries: number;
61
+ oldestEntry: string;
62
+ newestEntry: string;
63
+ historyFile: string;
64
+ queuedWrites: number;
65
+ };
66
+ /**
67
+ * Cleanup method - clears interval and flushes pending writes
68
+ * Call this during shutdown or in tests
69
+ */
70
+ cleanup(): Promise<void>;
71
+ }
72
+ export declare const toolHistory: ToolHistory;
73
+ export {};
@@ -0,0 +1,197 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ // Format timestamp in local timezone for display
5
+ function formatLocalTimestamp(isoTimestamp) {
6
+ const date = new Date(isoTimestamp);
7
+ return date.toLocaleString('en-US', {
8
+ year: 'numeric',
9
+ month: '2-digit',
10
+ day: '2-digit',
11
+ hour: '2-digit',
12
+ minute: '2-digit',
13
+ second: '2-digit',
14
+ hour12: false
15
+ });
16
+ }
17
+ class ToolHistory {
18
+ constructor() {
19
+ this.history = [];
20
+ this.MAX_ENTRIES = 1000;
21
+ this.writeQueue = [];
22
+ this.isWriting = false;
23
+ // Store history in same directory as config to keep everything together
24
+ const historyDir = path.join(os.homedir(), '.claude-server-commander');
25
+ // Ensure directory exists
26
+ if (!fs.existsSync(historyDir)) {
27
+ fs.mkdirSync(historyDir, { recursive: true });
28
+ }
29
+ // Use append-only JSONL format (JSON Lines)
30
+ this.historyFile = path.join(historyDir, 'tool-history.jsonl');
31
+ // Load existing history on startup
32
+ this.loadFromDisk();
33
+ // Start async write processor
34
+ this.startWriteProcessor();
35
+ }
36
+ /**
37
+ * Load history from disk (all instances share the same file)
38
+ */
39
+ loadFromDisk() {
40
+ try {
41
+ if (!fs.existsSync(this.historyFile)) {
42
+ console.error('[ToolHistory] No history file found, starting fresh');
43
+ return;
44
+ }
45
+ const content = fs.readFileSync(this.historyFile, 'utf-8');
46
+ const lines = content.trim().split('\n').filter(line => line.trim());
47
+ // Parse each line as JSON
48
+ const records = [];
49
+ for (const line of lines) {
50
+ try {
51
+ records.push(JSON.parse(line));
52
+ }
53
+ catch (e) {
54
+ console.error('[ToolHistory] Failed to parse line:', line);
55
+ }
56
+ }
57
+ // Keep only last 1000 entries
58
+ this.history = records.slice(-this.MAX_ENTRIES);
59
+ console.error(`[ToolHistory] Loaded ${this.history.length} entries from disk`);
60
+ // If file is getting too large, trim it
61
+ if (lines.length > this.MAX_ENTRIES * 2) {
62
+ this.trimHistoryFile();
63
+ }
64
+ }
65
+ catch (error) {
66
+ console.error('[ToolHistory] Failed to load history:', error);
67
+ }
68
+ }
69
+ /**
70
+ * Trim history file to prevent it from growing indefinitely
71
+ */
72
+ trimHistoryFile() {
73
+ try {
74
+ console.error('[ToolHistory] Trimming history file...');
75
+ // Keep last 1000 entries in memory
76
+ const keepEntries = this.history.slice(-this.MAX_ENTRIES);
77
+ // Write them back
78
+ const lines = keepEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
79
+ fs.writeFileSync(this.historyFile, lines, 'utf-8');
80
+ console.error(`[ToolHistory] Trimmed to ${keepEntries.length} entries`);
81
+ }
82
+ catch (error) {
83
+ console.error('[ToolHistory] Failed to trim history file:', error);
84
+ }
85
+ }
86
+ /**
87
+ * Async write processor - batches writes to avoid blocking
88
+ */
89
+ startWriteProcessor() {
90
+ this.writeInterval = setInterval(() => {
91
+ if (this.writeQueue.length > 0 && !this.isWriting) {
92
+ this.flushToDisk();
93
+ }
94
+ }, 1000); // Flush every second
95
+ // Prevent interval from keeping process alive during shutdown/tests
96
+ this.writeInterval.unref();
97
+ }
98
+ /**
99
+ * Flush queued writes to disk
100
+ */
101
+ async flushToDisk() {
102
+ if (this.isWriting || this.writeQueue.length === 0)
103
+ return;
104
+ this.isWriting = true;
105
+ const toWrite = [...this.writeQueue];
106
+ this.writeQueue = [];
107
+ try {
108
+ // Append to file (atomic append operation)
109
+ const lines = toWrite.map(entry => JSON.stringify(entry)).join('\n') + '\n';
110
+ fs.appendFileSync(this.historyFile, lines, 'utf-8');
111
+ }
112
+ catch (error) {
113
+ console.error('[ToolHistory] Failed to write to disk:', error);
114
+ // Put back in queue on failure
115
+ this.writeQueue.unshift(...toWrite);
116
+ }
117
+ finally {
118
+ this.isWriting = false;
119
+ }
120
+ }
121
+ /**
122
+ * Add a tool call to history
123
+ */
124
+ addCall(toolName, args, output, duration) {
125
+ const record = {
126
+ timestamp: new Date().toISOString(),
127
+ toolName,
128
+ arguments: args,
129
+ output,
130
+ duration
131
+ };
132
+ this.history.push(record);
133
+ // Keep only last 1000 in memory
134
+ if (this.history.length > this.MAX_ENTRIES) {
135
+ this.history.shift();
136
+ }
137
+ // Queue for async write
138
+ this.writeQueue.push(record);
139
+ }
140
+ /**
141
+ * Get recent tool calls with filters
142
+ */
143
+ getRecentCalls(options) {
144
+ let results = [...this.history];
145
+ // Filter by tool name
146
+ if (options.toolName) {
147
+ results = results.filter(r => r.toolName === options.toolName);
148
+ }
149
+ // Filter by timestamp
150
+ if (options.since) {
151
+ const sinceDate = new Date(options.since);
152
+ results = results.filter(r => new Date(r.timestamp) >= sinceDate);
153
+ }
154
+ // Limit results (default 50, max 1000)
155
+ const limit = Math.min(options.maxResults || 50, 1000);
156
+ return results.slice(-limit);
157
+ }
158
+ /**
159
+ * Get recent calls formatted with local timezone
160
+ */
161
+ getRecentCallsFormatted(options) {
162
+ const calls = this.getRecentCalls(options);
163
+ // Format timestamps to local timezone
164
+ return calls.map(call => ({
165
+ ...call,
166
+ timestamp: formatLocalTimestamp(call.timestamp)
167
+ }));
168
+ }
169
+ /**
170
+ * Get current stats
171
+ */
172
+ getStats() {
173
+ return {
174
+ totalEntries: this.history.length,
175
+ oldestEntry: this.history[0]?.timestamp,
176
+ newestEntry: this.history[this.history.length - 1]?.timestamp,
177
+ historyFile: this.historyFile,
178
+ queuedWrites: this.writeQueue.length
179
+ };
180
+ }
181
+ /**
182
+ * Cleanup method - clears interval and flushes pending writes
183
+ * Call this during shutdown or in tests
184
+ */
185
+ async cleanup() {
186
+ // Clear the interval
187
+ if (this.writeInterval) {
188
+ clearInterval(this.writeInterval);
189
+ this.writeInterval = undefined;
190
+ }
191
+ // Flush any remaining writes
192
+ if (this.writeQueue.length > 0) {
193
+ await this.flushToDisk();
194
+ }
195
+ }
196
+ }
197
+ export const toolHistory = new ToolHistory();
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.15";
1
+ export declare const VERSION = "0.2.17";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.15';
1
+ export const VERSION = '0.2.17';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonderwhy-er/desktop-commander",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "MCP server for terminal operations and file editing",
5
5
  "mcpName": "io.github.wonderwhy-er/desktop-commander",
6
6
  "license": "MIT",
@@ -42,6 +42,11 @@
42
42
  "link:local": "npm run build && npm link",
43
43
  "unlink:local": "npm unlink",
44
44
  "inspector": "npx @modelcontextprotocol/inspector dist/index.js",
45
+ "build:mcpb": "node scripts/build-mcpb.cjs",
46
+ "release": "node scripts/publish-release.cjs",
47
+ "release:minor": "node scripts/publish-release.cjs --minor",
48
+ "release:major": "node scripts/publish-release.cjs --major",
49
+ "release:dry": "node scripts/publish-release.cjs --dry-run",
45
50
  "logs:view": "npm run build && node scripts/view-fuzzy-logs.js",
46
51
  "logs:analyze": "npm run build && node scripts/analyze-fuzzy-logs.js",
47
52
  "logs:clear": "npm run build && node scripts/clear-fuzzy-logs.js",