@wonderwhy-er/desktop-commander 0.2.16 → 0.2.18-alpha.0

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 (47) hide show
  1. package/README.md +3 -5
  2. package/dist/data/spec-kit-prompts.json +123 -0
  3. package/dist/handlers/filesystem-handlers.js +5 -2
  4. package/dist/handlers/history-handlers.d.ts +5 -0
  5. package/dist/handlers/history-handlers.js +35 -0
  6. package/dist/handlers/index.d.ts +1 -0
  7. package/dist/handlers/index.js +1 -0
  8. package/dist/http-index.d.ts +45 -0
  9. package/dist/http-index.js +51 -0
  10. package/dist/http-server-auto-tunnel.d.ts +1 -0
  11. package/dist/http-server-auto-tunnel.js +667 -0
  12. package/dist/http-server-named-tunnel.d.ts +2 -0
  13. package/dist/http-server-named-tunnel.js +167 -0
  14. package/dist/http-server-tunnel.d.ts +2 -0
  15. package/dist/http-server-tunnel.js +111 -0
  16. package/dist/http-server.d.ts +2 -0
  17. package/dist/http-server.js +270 -0
  18. package/dist/index.js +4 -0
  19. package/dist/oauth/auth-middleware.d.ts +20 -0
  20. package/dist/oauth/auth-middleware.js +62 -0
  21. package/dist/oauth/index.d.ts +3 -0
  22. package/dist/oauth/index.js +3 -0
  23. package/dist/oauth/oauth-manager.d.ts +80 -0
  24. package/dist/oauth/oauth-manager.js +179 -0
  25. package/dist/oauth/oauth-routes.d.ts +3 -0
  26. package/dist/oauth/oauth-routes.js +377 -0
  27. package/dist/server.js +316 -210
  28. package/dist/setup-claude-server.js +29 -5
  29. package/dist/terminal-manager.d.ts +1 -1
  30. package/dist/terminal-manager.js +56 -1
  31. package/dist/tools/config.js +15 -1
  32. package/dist/tools/feedback.js +2 -2
  33. package/dist/tools/filesystem.d.ts +1 -1
  34. package/dist/tools/filesystem.js +51 -3
  35. package/dist/tools/improved-process-tools.js +179 -58
  36. package/dist/tools/schemas.d.ts +25 -0
  37. package/dist/tools/schemas.js +10 -0
  38. package/dist/types.d.ts +19 -0
  39. package/dist/utils/feature-flags.d.ts +43 -0
  40. package/dist/utils/feature-flags.js +147 -0
  41. package/dist/utils/toolHistory.d.ts +73 -0
  42. package/dist/utils/toolHistory.js +192 -0
  43. package/dist/utils/usageTracker.d.ts +4 -0
  44. package/dist/utils/usageTracker.js +63 -37
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +6 -1
@@ -44,7 +44,7 @@ async function initConfigFile() {
44
44
  // Ensure config directory exists
45
45
  const configDir = path.dirname(CONFIG_FILE);
46
46
  if (!existsSync(configDir)) {
47
- await mkdir(configDir, { recursive: true });
47
+ mkdirSync(configDir, { recursive: true });
48
48
  }
49
49
 
50
50
  // Check if config file exists
@@ -206,6 +206,28 @@ function detectShell() {
206
206
  return 'unknown-shell';
207
207
  }
208
208
 
209
+ // Function to get the package spec that was used to run this script
210
+ function getPackageSpec() {
211
+ // Check if running via npx - look for the package spec in process.argv
212
+ // e.g., npx @wonderwhy-er/desktop-commander@0.2.18-alpha setup
213
+ const argv = process.argv;
214
+
215
+ // Look for the package name in argv
216
+ for (let i = 0; i < argv.length; i++) {
217
+ const arg = argv[i];
218
+ if (arg.includes('@wonderwhy-er/desktop-commander')) {
219
+ // Extract just the package spec (e.g., @wonderwhy-er/desktop-commander@0.2.18-alpha)
220
+ const match = arg.match(/(@wonderwhy-er\/desktop-commander(@[^\/\s]+)?)/);
221
+ if (match) {
222
+ return match[1];
223
+ }
224
+ }
225
+ }
226
+
227
+ // Fallback to @latest if we can't detect
228
+ return '@wonderwhy-er/desktop-commander@latest';
229
+ }
230
+
209
231
  // Function to determine execution context
210
232
  function getExecutionContext() {
211
233
  // Check if running from npx
@@ -728,6 +750,7 @@ export default async function setup() {
728
750
  "DEBUG": "*"
729
751
  };
730
752
 
753
+ const packageSpec = getPackageSpec();
731
754
  serverConfig = {
732
755
  "command": isWindows ? "node.exe" : "node",
733
756
  "args": [
@@ -735,11 +758,11 @@ export default async function setup() {
735
758
  isWindows ?
736
759
  join(process.env.APPDATA || '', "npm", "npx.cmd").replace(/\\/g, '\\\\') :
737
760
  "$(which npx)",
738
- "@wonderwhy-er/desktop-commander@latest"
761
+ packageSpec
739
762
  ],
740
763
  "env": debugEnv
741
764
  };
742
- await trackEvent('npx_setup_config_debug_npx');
765
+ await trackEvent('npx_setup_config_debug_npx', { packageSpec });
743
766
  } else {
744
767
  // Debug with local installation path
745
768
  const indexPath = join(__dirname, 'dist', 'index.js');
@@ -763,13 +786,14 @@ export default async function setup() {
763
786
  } else {
764
787
  // Standard configuration without debug
765
788
  if (isNpx) {
789
+ const packageSpec = getPackageSpec();
766
790
  serverConfig = {
767
791
  "command": isWindows ? "npx.cmd" : "npx",
768
792
  "args": [
769
- "@wonderwhy-er/desktop-commander@latest"
793
+ packageSpec
770
794
  ]
771
795
  };
772
- await trackEvent('npx_setup_config_standard_npx');
796
+ await trackEvent('npx_setup_config_standard_npx', { packageSpec });
773
797
  } else {
774
798
  // For local installation, use absolute path to handle Windows properly
775
799
  const indexPath = join(__dirname, 'dist', 'index.js');
@@ -16,7 +16,7 @@ export declare class TerminalManager {
16
16
  * @returns Whether input was successfully sent
17
17
  */
18
18
  sendInputToProcess(pid: number, input: string): boolean;
19
- executeCommand(command: string, timeoutMs?: number, shell?: string): Promise<CommandExecutionResult>;
19
+ executeCommand(command: string, timeoutMs?: number, shell?: string, collectTiming?: boolean): Promise<CommandExecutionResult>;
20
20
  getNewOutput(pid: number): string | null;
21
21
  /**
22
22
  * Get a session by PID
@@ -33,7 +33,7 @@ export class TerminalManager {
33
33
  return false;
34
34
  }
35
35
  }
36
- async executeCommand(command, timeoutMs = DEFAULT_COMMAND_TIMEOUT, shell) {
36
+ async executeCommand(command, timeoutMs = DEFAULT_COMMAND_TIMEOUT, shell, collectTiming = false) {
37
37
  // Get the shell from config if not specified
38
38
  let shellToUse = shell;
39
39
  if (!shellToUse) {
@@ -81,6 +81,12 @@ export class TerminalManager {
81
81
  startTime: new Date()
82
82
  };
83
83
  this.sessions.set(childProcess.pid, session);
84
+ // Timing telemetry
85
+ const startTime = Date.now();
86
+ let firstOutputTime;
87
+ let lastOutputTime;
88
+ const outputEvents = [];
89
+ let exitReason = 'timeout';
84
90
  return new Promise((resolve) => {
85
91
  let resolved = false;
86
92
  let periodicCheck = null;
@@ -92,15 +98,47 @@ export class TerminalManager {
92
98
  resolved = true;
93
99
  if (periodicCheck)
94
100
  clearInterval(periodicCheck);
101
+ // Add timing info if requested
102
+ if (collectTiming) {
103
+ const endTime = Date.now();
104
+ result.timingInfo = {
105
+ startTime,
106
+ endTime,
107
+ totalDurationMs: endTime - startTime,
108
+ exitReason,
109
+ firstOutputTime,
110
+ lastOutputTime,
111
+ timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined,
112
+ outputEvents: outputEvents.length > 0 ? outputEvents : undefined
113
+ };
114
+ }
95
115
  resolve(result);
96
116
  };
97
117
  childProcess.stdout.on('data', (data) => {
98
118
  const text = data.toString();
119
+ const now = Date.now();
120
+ if (!firstOutputTime)
121
+ firstOutputTime = now;
122
+ lastOutputTime = now;
99
123
  output += text;
100
124
  session.lastOutput += text;
125
+ // Record output event if collecting timing
126
+ if (collectTiming) {
127
+ outputEvents.push({
128
+ timestamp: now,
129
+ deltaMs: now - startTime,
130
+ source: 'stdout',
131
+ length: text.length,
132
+ snippet: text.slice(0, 50).replace(/\n/g, '\\n')
133
+ });
134
+ }
101
135
  // Immediate check for obvious prompts
102
136
  if (quickPromptPatterns.test(text)) {
103
137
  session.isBlocked = true;
138
+ exitReason = 'early_exit_quick_pattern';
139
+ if (collectTiming && outputEvents.length > 0) {
140
+ outputEvents[outputEvents.length - 1].matchedPattern = 'quick_pattern';
141
+ }
104
142
  resolveOnce({
105
143
  pid: childProcess.pid,
106
144
  output,
@@ -110,8 +148,22 @@ export class TerminalManager {
110
148
  });
111
149
  childProcess.stderr.on('data', (data) => {
112
150
  const text = data.toString();
151
+ const now = Date.now();
152
+ if (!firstOutputTime)
153
+ firstOutputTime = now;
154
+ lastOutputTime = now;
113
155
  output += text;
114
156
  session.lastOutput += text;
157
+ // Record output event if collecting timing
158
+ if (collectTiming) {
159
+ outputEvents.push({
160
+ timestamp: now,
161
+ deltaMs: now - startTime,
162
+ source: 'stderr',
163
+ length: text.length,
164
+ snippet: text.slice(0, 50).replace(/\n/g, '\\n')
165
+ });
166
+ }
115
167
  });
116
168
  // Periodic comprehensive check every 100ms
117
169
  periodicCheck = setInterval(() => {
@@ -119,6 +171,7 @@ export class TerminalManager {
119
171
  const processState = analyzeProcessState(output, childProcess.pid);
120
172
  if (processState.isWaitingForInput) {
121
173
  session.isBlocked = true;
174
+ exitReason = 'early_exit_periodic_check';
122
175
  resolveOnce({
123
176
  pid: childProcess.pid,
124
177
  output,
@@ -130,6 +183,7 @@ export class TerminalManager {
130
183
  // Timeout fallback
131
184
  setTimeout(() => {
132
185
  session.isBlocked = true;
186
+ exitReason = 'timeout';
133
187
  resolveOnce({
134
188
  pid: childProcess.pid,
135
189
  output,
@@ -153,6 +207,7 @@ export class TerminalManager {
153
207
  }
154
208
  this.sessions.delete(childProcess.pid);
155
209
  }
210
+ exitReason = 'process_exit';
156
211
  resolveOnce({
157
212
  pid: childProcess.pid,
158
213
  output,
@@ -2,6 +2,7 @@ import { configManager } from '../config-manager.js';
2
2
  import { SetConfigValueArgsSchema } from './schemas.js';
3
3
  import { getSystemInfo } from '../utils/system-info.js';
4
4
  import { currentClient } from '../server.js';
5
+ import { featureFlagManager } from '../utils/feature-flags.js';
5
6
  /**
6
7
  * Get the entire config including system information
7
8
  */
@@ -11,10 +12,23 @@ export async function getConfig() {
11
12
  const config = await configManager.getConfig();
12
13
  // Add system information and current client to the config response
13
14
  const systemInfo = getSystemInfo();
15
+ // Get memory usage
16
+ const memoryUsage = process.memoryUsage();
17
+ const memory = {
18
+ rss: `${(memoryUsage.rss / 1024 / 1024).toFixed(2)} MB`,
19
+ heapTotal: `${(memoryUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
20
+ heapUsed: `${(memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
21
+ external: `${(memoryUsage.external / 1024 / 1024).toFixed(2)} MB`,
22
+ arrayBuffers: `${(memoryUsage.arrayBuffers / 1024 / 1024).toFixed(2)} MB`
23
+ };
14
24
  const configWithSystemInfo = {
15
25
  ...config,
16
26
  currentClient,
17
- systemInfo
27
+ featureFlags: featureFlagManager.getAll(),
28
+ systemInfo: {
29
+ ...systemInfo,
30
+ memory
31
+ }
18
32
  };
19
33
  console.error(`getConfig result: ${JSON.stringify(configWithSystemInfo, null, 2)}`);
20
34
  return {
@@ -73,7 +73,7 @@ export async function giveFeedbackToDesktopCommander(params = {}) {
73
73
  content: [{
74
74
  type: "text",
75
75
  text: `❌ **Error opening feedback form**: ${error instanceof Error ? error.message : String(error)}\n\n` +
76
- `You can still access our feedback form directly at: https://tally.so/r/mYB6av\n\n` +
76
+ `You can still access our feedback form directly at: https://tally.so/r/mKqoKg\n\n` +
77
77
  `We appreciate your willingness to provide feedback!`
78
78
  }],
79
79
  isError: true
@@ -84,7 +84,7 @@ export async function giveFeedbackToDesktopCommander(params = {}) {
84
84
  * Build Tally.so URL with pre-filled parameters
85
85
  */
86
86
  async function buildTallyUrl(params, stats) {
87
- const baseUrl = 'https://tally.so/r/mYB6av';
87
+ const baseUrl = 'https://tally.so/r/mKqoKg';
88
88
  const urlParams = new URLSearchParams();
89
89
  // Only auto-filled hidden fields remain
90
90
  urlParams.set('tool_call_count', stats.totalToolCalls.toString());
@@ -56,7 +56,7 @@ export interface MultiFileResult {
56
56
  }
57
57
  export declare function readMultipleFiles(paths: string[]): Promise<MultiFileResult[]>;
58
58
  export declare function createDirectory(dirPath: string): Promise<void>;
59
- export declare function listDirectory(dirPath: string): Promise<string[]>;
59
+ export declare function listDirectory(dirPath: string, depth?: number): Promise<string[]>;
60
60
  export declare function moveFile(sourcePath: string, destinationPath: string): Promise<void>;
61
61
  export declare function searchFiles(rootPath: string, pattern: string): Promise<string[]>;
62
62
  export declare function getFileInfo(filePath: string): Promise<Record<string, any>>;
@@ -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);