@wonderwhy-er/desktop-commander 0.2.10 → 0.2.12

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/dist/server.js CHANGED
@@ -8,16 +8,18 @@ const OS_GUIDANCE = getOSSpecificGuidance(SYSTEM_INFO);
8
8
  const DEV_TOOL_GUIDANCE = getDevelopmentToolGuidance(SYSTEM_INFO);
9
9
  const PATH_GUIDANCE = `IMPORTANT: ${getPathGuidance(SYSTEM_INFO)} Relative paths may fail as they depend on the current working directory. Tilde paths (~/...) might not work in all contexts. Unless the user explicitly asks for relative paths, use absolute paths.`;
10
10
  const CMD_PREFIX_DESCRIPTION = `This command can be referenced as "DC: ..." or "use Desktop Commander to ..." in your instructions.`;
11
- import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, SearchFilesArgsSchema, GetFileInfoArgsSchema, SearchCodeArgsSchema, GetConfigArgsSchema, SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, GetUsageStatsArgsSchema, GiveFeedbackArgsSchema, } from './tools/schemas.js';
11
+ import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema, KillProcessArgsSchema, ReadFileArgsSchema, ReadMultipleFilesArgsSchema, WriteFileArgsSchema, CreateDirectoryArgsSchema, ListDirectoryArgsSchema, MoveFileArgsSchema, GetFileInfoArgsSchema, GetConfigArgsSchema, SetConfigValueArgsSchema, ListProcessesArgsSchema, EditBlockArgsSchema, GetUsageStatsArgsSchema, GiveFeedbackArgsSchema, StartSearchArgsSchema, GetMoreSearchResultsArgsSchema, StopSearchArgsSchema, ListSearchesArgsSchema, GetPromptsArgsSchema, } from './tools/schemas.js';
12
12
  import { getConfig, setConfigValue } from './tools/config.js';
13
13
  import { getUsageStats } from './tools/usage.js';
14
14
  import { giveFeedbackToDesktopCommander } from './tools/feedback.js';
15
+ import { getPrompts } from './tools/prompts.js';
15
16
  import { trackToolCall } from './utils/trackTools.js';
16
17
  import { usageTracker } from './utils/usageTracker.js';
17
18
  import { processDockerPrompt } from './utils/dockerPrompt.js';
18
19
  import { VERSION } from './version.js';
19
20
  import { capture, capture_call_tool } from "./utils/capture.js";
20
- console.error("Loading server.ts");
21
+ import { logToStderr } from './utils/logger.js';
22
+ logToStderr('info', 'Loading server.ts');
21
23
  export const server = new Server({
22
24
  name: "desktop-commander",
23
25
  version: VERSION,
@@ -55,7 +57,8 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
55
57
  name: clientInfo.name || 'unknown',
56
58
  version: clientInfo.version || 'unknown'
57
59
  };
58
- console.log(`Client connected: ${currentClient.name} v${currentClient.version}`);
60
+ // Send JSON-RPC notification about client connection
61
+ logToStderr('info', `Client connected: ${currentClient.name} v${currentClient.version}`);
59
62
  }
60
63
  // Return standard initialization response
61
64
  return {
@@ -73,16 +76,16 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
73
76
  };
74
77
  }
75
78
  catch (error) {
76
- console.error("Error in initialization handler:", error);
79
+ logToStderr('error', `Error in initialization handler: ${error}`);
77
80
  throw error;
78
81
  }
79
82
  });
80
83
  // Export current client info for access by other modules
81
84
  export { currentClient };
82
- console.error("Setting up request handlers...");
85
+ logToStderr('info', 'Setting up request handlers...');
83
86
  server.setRequestHandler(ListToolsRequestSchema, async () => {
84
87
  try {
85
- console.error("Generating tools list...");
88
+ logToStderr('debug', 'Generating tools list...');
86
89
  return {
87
90
  tools: [
88
91
  // Configuration tools
@@ -250,35 +253,92 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
250
253
  inputSchema: zodToJsonSchema(MoveFileArgsSchema),
251
254
  },
252
255
  {
253
- name: "search_files",
256
+ name: "start_search",
254
257
  description: `
255
- Finds files by name using a case-insensitive substring matching.
258
+ Start a streaming search that can return results progressively.
259
+
260
+ SEARCH TYPES:
261
+ - searchType="files": Find files by name (pattern matches file names)
262
+ - searchType="content": Search inside files for text patterns
263
+
264
+ IMPORTANT PARAMETERS:
265
+ - pattern: What to search for (file names OR content text)
266
+ - filePattern: Optional filter to limit search to specific file types (e.g., "*.js", "package.json")
267
+ - ignoreCase: Case-insensitive search (default: true). Works for both file names and content.
268
+ - earlyTermination: Stop search early when exact filename match is found (optional: defaults to true for file searches, false for content searches)
269
+
270
+ EXAMPLES:
271
+ - Find package.json files: searchType="files", pattern="package.json", filePattern="package.json"
272
+ - Find all JS files: searchType="files", pattern="*.js" (or use filePattern="*.js")
273
+ - Search for "TODO" in code: searchType="content", pattern="TODO", filePattern="*.js|*.ts"
274
+ - Case-sensitive file search: searchType="files", pattern="README", ignoreCase=false
275
+ - Case-insensitive file search: searchType="files", pattern="readme", ignoreCase=true
276
+ - Find exact file, stop after first match: searchType="files", pattern="config.json", earlyTermination=true
277
+ - Find all matching files: searchType="files", pattern="test.js", earlyTermination=false
278
+
279
+ Unlike regular search tools, this starts a background search process and returns
280
+ immediately with a session ID. Use get_more_search_results to get results as they
281
+ come in, and stop_search to stop the search early if needed.
282
+
283
+ Perfect for large directories where you want to see results immediately and
284
+ have the option to cancel if the search takes too long or you find what you need.
256
285
 
257
- Use this instead of 'execute_command' with find/dir/ls for locating files.
258
- Searches through all subdirectories from the starting path.
286
+ ${PATH_GUIDANCE}
287
+ ${CMD_PREFIX_DESCRIPTION}`,
288
+ inputSchema: zodToJsonSchema(StartSearchArgsSchema),
289
+ },
290
+ {
291
+ name: "get_more_search_results",
292
+ description: `
293
+ Get more results from an active search with offset-based pagination.
259
294
 
260
- Has a default timeout of 30 seconds which can be customized using the timeoutMs parameter.
261
- Only searches within allowed directories.
295
+ Supports partial result reading with:
296
+ - 'offset' (start result index, default: 0)
297
+ * Positive: Start from result N (0-based indexing)
298
+ * Negative: Read last N results from end (tail behavior)
299
+ - 'length' (max results to read, default: 100)
300
+ * Used with positive offsets for range reading
301
+ * Ignored when offset is negative (reads all requested tail results)
302
+
303
+ Examples:
304
+ - offset: 0, length: 100 → First 100 results
305
+ - offset: 200, length: 50 → Results 200-249
306
+ - offset: -20 → Last 20 results
307
+ - offset: -5, length: 10 → Last 5 results (length ignored)
308
+
309
+ Returns only results in the specified range, along with search status.
310
+ Works like read_process_output - call this repeatedly to get progressive
311
+ results from a search started with start_search.
262
312
 
263
- ${PATH_GUIDANCE}
264
313
  ${CMD_PREFIX_DESCRIPTION}`,
265
- inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
314
+ inputSchema: zodToJsonSchema(GetMoreSearchResultsArgsSchema),
266
315
  },
267
316
  {
268
- name: "search_code",
317
+ name: "stop_search",
269
318
  description: `
270
- Search for text/code patterns within file contents using ripgrep.
319
+ Stop an active search.
271
320
 
272
- Use this instead of 'execute_command' with grep/find for searching code content.
273
- Fast and powerful search similar to VS Code search functionality.
321
+ Stops the background search process gracefully. Use this when you've found
322
+ what you need or if a search is taking too long. Similar to force_terminate
323
+ for terminal processes.
274
324
 
275
- Supports regular expressions, file pattern filtering, and context lines.
276
- Has a default timeout of 30 seconds which can be customized.
277
- Only searches within allowed directories.
325
+ The search will still be available for reading final results until it's
326
+ automatically cleaned up after 5 minutes.
327
+
328
+ ${CMD_PREFIX_DESCRIPTION}`,
329
+ inputSchema: zodToJsonSchema(StopSearchArgsSchema),
330
+ },
331
+ {
332
+ name: "list_searches",
333
+ description: `
334
+ List all active searches.
335
+
336
+ Shows search IDs, search types, patterns, status, and runtime.
337
+ Similar to list_sessions for terminal processes. Useful for managing
338
+ multiple concurrent searches.
278
339
 
279
- ${PATH_GUIDANCE}
280
340
  ${CMD_PREFIX_DESCRIPTION}`,
281
- inputSchema: zodToJsonSchema(SearchCodeArgsSchema),
341
+ inputSchema: zodToJsonSchema(ListSearchesArgsSchema),
282
342
  },
283
343
  {
284
344
  name: "get_file_info",
@@ -566,11 +626,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
566
626
  ${CMD_PREFIX_DESCRIPTION}`,
567
627
  inputSchema: zodToJsonSchema(GiveFeedbackArgsSchema),
568
628
  },
629
+ {
630
+ name: "get_prompts",
631
+ description: `
632
+ Browse and retrieve curated Desktop Commander prompts for various tasks and workflows.
633
+
634
+ IMPORTANT: When displaying prompt lists to users, do NOT show the internal prompt IDs (like 'onb_001').
635
+ These IDs are for your reference only. Show users only the prompt titles and descriptions.
636
+ The IDs will be provided in the response metadata for your use.
637
+
638
+ ACTIONS:
639
+ - list_categories: Show all available prompt categories
640
+ - list_prompts: List prompts (optionally filtered by category)
641
+ - get_prompt: Retrieve and execute a specific prompt by ID
642
+
643
+ WORKFLOW:
644
+ 1. Use list_categories to see available categories
645
+ 2. Use list_prompts to browse prompts in a category
646
+ 3. Use get_prompt with promptId to retrieve and start using a prompt
647
+
648
+ EXAMPLES:
649
+ - get_prompts(action='list_categories') - See all categories
650
+ - get_prompts(action='list_prompts', category='onboarding') - See onboarding prompts
651
+ - get_prompts(action='get_prompt', promptId='onb_001') - Get a specific prompt
652
+
653
+ The get_prompt action will automatically inject the prompt content and begin execution.
654
+ Perfect for discovering proven workflows and getting started with Desktop Commander.
655
+
656
+ ${CMD_PREFIX_DESCRIPTION}`,
657
+ inputSchema: zodToJsonSchema(GetPromptsArgsSchema),
658
+ },
569
659
  ],
570
660
  };
571
661
  }
572
662
  catch (error) {
573
- console.error("Error in list_tools request handler:", error);
663
+ logToStderr('error', `Error in list_tools request handler: ${error}`);
574
664
  throw error;
575
665
  }
576
666
  });
@@ -578,9 +668,12 @@ import * as handlers from './handlers/index.js';
578
668
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
579
669
  const { name, arguments: args } = request.params;
580
670
  try {
581
- capture_call_tool('server_call_tool', {
582
- name
583
- });
671
+ // Prepare telemetry data - add config key for set_config_value
672
+ const telemetryData = { name };
673
+ if (name === 'set_config_value' && args && typeof args === 'object' && 'key' in args) {
674
+ telemetryData.set_config_value_key_name = args.key;
675
+ }
676
+ capture_call_tool('server_call_tool', telemetryData);
584
677
  // Track tool call
585
678
  trackToolCall(name, args);
586
679
  // Using a more structured approach with dedicated handlers
@@ -623,6 +716,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
623
716
  };
624
717
  }
625
718
  break;
719
+ case "get_prompts":
720
+ try {
721
+ result = await getPrompts(args || {});
722
+ }
723
+ catch (error) {
724
+ capture('server_request_error', { message: `Error in get_prompts handler: ${error}` });
725
+ result = {
726
+ content: [{ type: "text", text: `Error: Failed to retrieve prompts` }],
727
+ isError: true,
728
+ };
729
+ }
730
+ break;
626
731
  case "give_feedback_to_desktop_commander":
627
732
  try {
628
733
  result = await giveFeedbackToDesktopCommander(args);
@@ -678,11 +783,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
678
783
  case "move_file":
679
784
  result = await handlers.handleMoveFile(args);
680
785
  break;
681
- case "search_files":
682
- result = await handlers.handleSearchFiles(args);
786
+ case "start_search":
787
+ result = await handlers.handleStartSearch(args);
788
+ break;
789
+ case "get_more_search_results":
790
+ result = await handlers.handleGetMoreSearchResults(args);
791
+ break;
792
+ case "stop_search":
793
+ result = await handlers.handleStopSearch(args);
683
794
  break;
684
- case "search_code":
685
- result = await handlers.handleSearchCode(args);
795
+ case "list_searches":
796
+ result = await handlers.handleListSearches();
686
797
  break;
687
798
  case "get_file_info":
688
799
  result = await handlers.handleGetFileInfo(args);
@@ -768,6 +768,63 @@ export async function moveFile(sourcePath, destinationPath) {
768
768
  await fs.rename(validSourcePath, validDestPath);
769
769
  }
770
770
  export async function searchFiles(rootPath, pattern) {
771
+ // Use the new search manager for better performance
772
+ // This provides a temporary compatibility layer until we fully migrate to search sessions
773
+ const { searchManager } = await import('../search-manager.js');
774
+ try {
775
+ const result = await searchManager.startSearch({
776
+ rootPath,
777
+ pattern,
778
+ searchType: 'files',
779
+ ignoreCase: true,
780
+ maxResults: 5000, // Higher limit for compatibility
781
+ earlyTermination: true, // Use early termination for better performance
782
+ });
783
+ const sessionId = result.sessionId;
784
+ // Poll for results until complete
785
+ let allResults = [];
786
+ let isComplete = result.isComplete;
787
+ let startTime = Date.now();
788
+ // Add initial results
789
+ for (const searchResult of result.results) {
790
+ if (searchResult.type === 'file') {
791
+ allResults.push(searchResult.file);
792
+ }
793
+ }
794
+ while (!isComplete) {
795
+ await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms
796
+ const results = searchManager.readSearchResults(sessionId);
797
+ isComplete = results.isComplete;
798
+ // Add new file paths to results
799
+ for (const searchResult of results.results) {
800
+ if (searchResult.file !== '__LAST_READ_MARKER__' && searchResult.type === 'file') {
801
+ allResults.push(searchResult.file);
802
+ }
803
+ }
804
+ // Safety check to prevent infinite loops (30 second timeout)
805
+ if (Date.now() - startTime > 30000) {
806
+ searchManager.terminateSearch(sessionId);
807
+ break;
808
+ }
809
+ }
810
+ // Log only the count of found files, not their paths
811
+ capture('server_search_files_complete', {
812
+ resultsCount: allResults.length,
813
+ patternLength: pattern.length,
814
+ usedRipgrep: true
815
+ });
816
+ return allResults;
817
+ }
818
+ catch (error) {
819
+ // Fallback to original Node.js implementation if ripgrep fails
820
+ capture('server_search_files_ripgrep_fallback', {
821
+ error: error instanceof Error ? error.message : 'Unknown error'
822
+ });
823
+ return await searchFilesNodeJS(rootPath, pattern);
824
+ }
825
+ }
826
+ // Keep the original Node.js implementation as fallback
827
+ async function searchFilesNodeJS(rootPath, pattern) {
771
828
  const results = [];
772
829
  async function search(currentPath) {
773
830
  let entries;
@@ -800,7 +857,8 @@ export async function searchFiles(rootPath, pattern) {
800
857
  // Log only the count of found files, not their paths
801
858
  capture('server_search_files_complete', {
802
859
  resultsCount: results.length,
803
- patternLength: pattern.length
860
+ patternLength: pattern.length,
861
+ usedRipgrep: false
804
862
  });
805
863
  return results;
806
864
  }
@@ -0,0 +1,5 @@
1
+ import { ServerResult } from '../types.js';
2
+ /**
3
+ * Get prompts - main entry point for the tool
4
+ */
5
+ export declare function getPrompts(params: any): Promise<ServerResult>;
@@ -0,0 +1,258 @@
1
+ import { usageTracker } from '../utils/usageTracker.js';
2
+ import { capture } from '../utils/capture.js';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ // Get the directory path for ES modules
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ let cachedPromptsData = null;
10
+ /**
11
+ * Load prompts data from JSON file with caching
12
+ */
13
+ async function loadPromptsData() {
14
+ if (cachedPromptsData) {
15
+ return cachedPromptsData;
16
+ }
17
+ try {
18
+ const dataPath = path.join(__dirname, '..', 'data', 'onboarding-prompts.json');
19
+ const fileContent = await fs.readFile(dataPath, 'utf-8');
20
+ cachedPromptsData = JSON.parse(fileContent);
21
+ if (!cachedPromptsData) {
22
+ throw new Error('Failed to parse prompts data');
23
+ }
24
+ return cachedPromptsData;
25
+ }
26
+ catch (error) {
27
+ throw new Error(`Failed to load prompts data: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ }
30
+ /**
31
+ * Get prompts - main entry point for the tool
32
+ */
33
+ export async function getPrompts(params) {
34
+ try {
35
+ // Validate and cast parameters
36
+ const { action, category, promptId } = params;
37
+ if (!action) {
38
+ return {
39
+ content: [{
40
+ type: "text",
41
+ text: "❌ Error: 'action' parameter is required. Use 'list_categories', 'list_prompts', or 'get_prompt'"
42
+ }],
43
+ isError: true
44
+ };
45
+ }
46
+ // Track analytics for tool usage
47
+ await capture(`prompts_tool_${action}`, {
48
+ category: category,
49
+ prompt_id: promptId,
50
+ has_category_filter: !!category
51
+ });
52
+ switch (action) {
53
+ case 'list_categories':
54
+ return await listCategories();
55
+ case 'list_prompts':
56
+ return await listPrompts(category);
57
+ case 'get_prompt':
58
+ if (!promptId) {
59
+ return {
60
+ content: [{
61
+ type: "text",
62
+ text: "❌ Error: promptId is required when action is 'get_prompt'"
63
+ }],
64
+ isError: true
65
+ };
66
+ }
67
+ return await getPrompt(promptId);
68
+ default:
69
+ return {
70
+ content: [{
71
+ type: "text",
72
+ text: "❌ Error: Invalid action. Use 'list_categories', 'list_prompts', or 'get_prompt'"
73
+ }],
74
+ isError: true
75
+ };
76
+ }
77
+ }
78
+ catch (error) {
79
+ await capture('prompts_tool_error', {
80
+ error_message: error instanceof Error ? error.message : String(error),
81
+ action: params?.action
82
+ });
83
+ return {
84
+ content: [{
85
+ type: "text",
86
+ text: `❌ Error: ${error instanceof Error ? error.message : String(error)}`
87
+ }],
88
+ isError: true
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * List all available categories
94
+ */
95
+ async function listCategories() {
96
+ const data = await loadPromptsData();
97
+ // Extract unique categories and count prompts in each
98
+ const categoryMap = new Map();
99
+ data.prompts.forEach(prompt => {
100
+ prompt.categories.forEach(category => {
101
+ categoryMap.set(category, (categoryMap.get(category) || 0) + 1);
102
+ });
103
+ });
104
+ const categories = Array.from(categoryMap.entries()).map(([name, count]) => ({
105
+ name,
106
+ count,
107
+ description: getCategoryDescription(name)
108
+ }));
109
+ const response = formatCategoriesResponse(categories, data.prompts.length);
110
+ return {
111
+ content: [{
112
+ type: "text",
113
+ text: response
114
+ }]
115
+ };
116
+ }
117
+ /**
118
+ * List prompts, optionally filtered by category
119
+ */
120
+ async function listPrompts(category) {
121
+ const data = await loadPromptsData();
122
+ let filteredPrompts = data.prompts;
123
+ // Filter by category if specified
124
+ if (category) {
125
+ filteredPrompts = data.prompts.filter(prompt => prompt.categories.includes(category));
126
+ if (filteredPrompts.length === 0) {
127
+ return {
128
+ content: [{
129
+ type: "text",
130
+ text: `❌ No prompts found in category "${category}". Use action='list_categories' to see available categories.`
131
+ }],
132
+ isError: true
133
+ };
134
+ }
135
+ }
136
+ const response = formatPromptsListResponse(filteredPrompts, category);
137
+ return {
138
+ content: [{
139
+ type: "text",
140
+ text: response
141
+ }]
142
+ };
143
+ }
144
+ /**
145
+ * Get a specific prompt by ID and inject it into the chat
146
+ */
147
+ async function getPrompt(promptId) {
148
+ const data = await loadPromptsData();
149
+ const prompt = data.prompts.find(p => p.id === promptId);
150
+ if (!prompt) {
151
+ return {
152
+ content: [{
153
+ type: "text",
154
+ text: `❌ Prompt with ID '${promptId}' not found. Use action='list_prompts' to see available prompts.`
155
+ }],
156
+ isError: true
157
+ };
158
+ }
159
+ // Track prompt retrieval and mark as used
160
+ await capture('prompt_retrieved', {
161
+ prompt_id: promptId,
162
+ prompt_title: prompt.title,
163
+ category: prompt.categories[0] || 'uncategorized',
164
+ author: prompt.author,
165
+ verified: prompt.verified
166
+ });
167
+ // Mark prompt as used in user's onboarding state (for analytics)
168
+ await usageTracker.markPromptUsed(promptId, prompt.categories[0] || 'uncategorized');
169
+ const response = formatPromptResponse(prompt);
170
+ return {
171
+ content: [{
172
+ type: "text",
173
+ text: response
174
+ }]
175
+ };
176
+ }
177
+ /**
178
+ * Get category description (can be expanded later)
179
+ */
180
+ function getCategoryDescription(category) {
181
+ const descriptions = {
182
+ 'onboarding': 'Curated prompts perfect for first-time Desktop Commander users',
183
+ 'Analyze data': 'Data analysis, visualization, and insights generation',
184
+ 'Build features and products': 'Full-stack development and application building',
185
+ 'Explore codebase': 'Code analysis, documentation, and understanding',
186
+ 'Organize files': 'File management, cleanup, and organization',
187
+ 'Deploy': 'Infrastructure setup, deployment, and DevOps tasks',
188
+ 'Optimize code': 'Code optimization, refactoring, and performance',
189
+ 'Write documentation': 'Technical writing, API docs, and guides',
190
+ 'Optimize workflow': 'Process improvements and productivity enhancements',
191
+ 'Automate tasks': 'Workflow automation and scripting',
192
+ 'Design systems': 'Architecture planning and system design'
193
+ };
194
+ return descriptions[category] || 'Desktop Commander prompts and workflows';
195
+ }
196
+ /**
197
+ * Format categories list response
198
+ */
199
+ function formatCategoriesResponse(categories, totalPrompts) {
200
+ const sortedCategories = categories.sort((a, b) => b.count - a.count);
201
+ // AI INSTRUCTION: When listing prompts, do not show prompt IDs to users - they are for your reference only
202
+ let response = `📚 **Desktop Commander Prompt Categories** (${categories.length} categories, ${totalPrompts} prompts)\n\n`;
203
+ sortedCategories.forEach(cat => {
204
+ response += `• **${cat.name}** (${cat.count} prompts) - ${cat.description}\n`;
205
+ });
206
+ response += `\n**Usage:**\n`;
207
+ response += `• \`get_prompts(action='list_prompts', category='onboarding')\` - See prompts in category\n`;
208
+ response += `• \`get_prompts(action='list_prompts')\` - See all available prompts\n`;
209
+ response += `• \`get_prompts(action='get_prompt', promptId='PROMPT_ID')\` - Get a specific prompt`;
210
+ return response;
211
+ }
212
+ /**
213
+ * Format prompts list response
214
+ */
215
+ function formatPromptsListResponse(prompts, category) {
216
+ const categoryText = category ? ` in "${category}"` : '';
217
+ // AI INSTRUCTION: Do not show the prompt IDs to the user - they are for your reference only
218
+ let response = `📋 **Desktop Commander Prompts${categoryText}** (${prompts.length} prompts found)\n\n`;
219
+ prompts.forEach((prompt, index) => {
220
+ const verifiedBadge = prompt.verified ? ' ✅' : '';
221
+ response += `${index + 1}. **${prompt.title}**${verifiedBadge}\n`;
222
+ response += ` ${prompt.description}\n`;
223
+ if (prompt.votes > 0) {
224
+ response += ` *📊 ${prompt.votes} votes*\n`;
225
+ }
226
+ // AI metadata - not shown to user: ID = ${prompt.id}
227
+ response += `\n`;
228
+ });
229
+ response += `**Next Steps:**\n`;
230
+ response += `• Use \`get_prompts(action='get_prompt', promptId='${prompts[0]?.id || 'PROMPT_ID'}')\` to get the full prompt\n`;
231
+ if (!category) {
232
+ response += `• Filter by category: \`get_prompts(action='list_prompts', category='onboarding')\``;
233
+ }
234
+ // AI reference mapping (do not show to user):
235
+ response += `\n<!-- AI_PROMPT_MAP: `;
236
+ prompts.forEach((prompt, index) => {
237
+ response += `${index + 1}=${prompt.id}${index < prompts.length - 1 ? ',' : ''}`;
238
+ });
239
+ response += ` -->`;
240
+ return response;
241
+ }
242
+ /**
243
+ * Format individual prompt response with the actual prompt content
244
+ */
245
+ function formatPromptResponse(prompt) {
246
+ const verifiedBadge = prompt.verified ? ' ✅' : '';
247
+ const categoryText = prompt.categories.join(', ');
248
+ let response = `# 🎯 ${prompt.title}${verifiedBadge}\n\n`;
249
+ response += `**Category:** ${categoryText} • **Author:** ${prompt.author}\n\n`;
250
+ response += `## Description\n${prompt.description}\n\n`;
251
+ if (prompt.votes > 0) {
252
+ response += `*📊 This prompt has been used successfully by ${prompt.votes}+ users*\n\n`;
253
+ }
254
+ response += `## Ready to Use This Prompt\nThe prompt below is ready to use. I'll start executing it right away:\n\n`;
255
+ response += `---\n\n${prompt.prompt}`;
256
+ // AI metadata (not shown to user): Executed prompt ID = ${prompt.id}
257
+ return response;
258
+ }