Skip to content

Commit 5c09672

Browse files
jqnatividadclaude
andcommitted
feat: add auto-detection of qsv binary path for Desktop Extension
Implements smart auto-detection to eliminate manual path configuration: - Auto-detects qsv binary from PATH using which/where - Falls back to common installation locations on failure: - macOS/Linux: /usr/local/bin, /opt/homebrew/bin, ~/.cargo/bin, etc. - Windows: C:\Program Files\qsv, scoop, AppData, etc. - Makes qsv_path and working_dir optional in manifest with smart defaults - Adds qsv_config tool to display resolved configuration and diagnostics - Fixes ES module import bug (require('fs') → import { statSync }) The extension now works out-of-the-box with zero configuration when qsv is installed in standard locations. Users can still manually specify paths if needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6e31812 commit 5c09672

File tree

4 files changed

+257
-13
lines changed

4 files changed

+257
-13
lines changed

.claude/skills/manifest.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,16 @@
6161
"qsv_path": {
6262
"type": "file",
6363
"title": "qsv Binary Path",
64-
"description": "Full path to qsv executable. Click 'Browse' to locate it (usually /usr/local/bin/qsv or /opt/homebrew/bin/qsv on macOS). Install from: https://github.com/dathere/qsv#installation",
65-
"required": true
64+
"description": "Full path to qsv executable. Leave empty to auto-detect from PATH, or click 'Browse' to specify (usually /usr/local/bin/qsv or /opt/homebrew/bin/qsv on macOS). Install from: https://github.com/dathere/qsv#installation",
65+
"required": false,
66+
"default": ""
6667
},
6768
"working_dir": {
6869
"type": "directory",
6970
"title": "Default Working Directory",
70-
"description": "Directory where qsv commands run by default. Click 'Browse' to select your Downloads folder or another directory. Extension only accesses files within this directory and allowed directories below.",
71-
"required": true
71+
"description": "Directory where qsv commands run by default. Defaults to Downloads folder. Click 'Browse' to change. Extension only accesses files within this directory and allowed directories below.",
72+
"required": false,
73+
"default": "${HOME}/Downloads"
7274
},
7375
"allowed_dirs": {
7476
"type": "directory",
@@ -118,6 +120,10 @@
118120
}
119121
},
120122
"tools": [
123+
{
124+
"name": "qsv_config",
125+
"description": "Display current qsv configuration (binary path, version, directories)"
126+
},
121127
{
122128
"name": "qsv_select",
123129
"description": "Select, reorder, or exclude columns from CSV data"

.claude/skills/src/config.ts

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { homedir, tmpdir } from 'os';
99
import { join } from 'path';
1010
import { execSync, execFileSync } from 'child_process';
11+
import { statSync } from 'fs';
1112

1213
/**
1314
* Timeout for qsv binary validation commands in milliseconds (5 seconds)
@@ -165,23 +166,111 @@ export interface QsvValidationResult {
165166
error?: string;
166167
}
167168

169+
// Global diagnostic info for auto-detection
170+
let lastDetectionDiagnostics: {
171+
whichAttempted: boolean;
172+
whichResult?: string;
173+
whichError?: string;
174+
locationsChecked: Array<{
175+
path: string;
176+
exists: boolean;
177+
isFile?: boolean;
178+
executable?: boolean;
179+
error?: string;
180+
version?: string;
181+
}>;
182+
} = {
183+
whichAttempted: false,
184+
locationsChecked: []
185+
};
186+
187+
/**
188+
* Get diagnostic information about the last auto-detection attempt
189+
*/
190+
export function getDetectionDiagnostics() {
191+
return lastDetectionDiagnostics;
192+
}
193+
168194
/**
169195
* Auto-detect absolute path to qsv binary
170-
* Uses 'which' on Unix/macOS or 'where' on Windows
196+
* 1. Uses 'which' on Unix/macOS or 'where' on Windows
197+
* 2. If that fails, checks common installation locations
171198
*/
172199
function detectQsvBinaryPath(): string | null {
200+
// Reset diagnostics
201+
lastDetectionDiagnostics = {
202+
whichAttempted: true,
203+
locationsChecked: []
204+
};
205+
206+
// Try using which/where first
173207
try {
174-
// Use execFileSync instead of execSync for security best practice
175208
const command = process.platform === 'win32' ? 'where' : 'which';
176209
const result = execFileSync(command, ['qsv'], {
177210
encoding: 'utf8',
178211
stdio: ['ignore', 'pipe', 'ignore']
179212
});
180213
const path = result.trim().split('\n')[0]; // Take first result
181-
return path || null;
182-
} catch {
183-
return null;
214+
if (path) {
215+
lastDetectionDiagnostics.whichResult = path;
216+
return path;
217+
}
218+
} catch (error) {
219+
lastDetectionDiagnostics.whichError = error instanceof Error ? error.message : String(error);
184220
}
221+
222+
// Check common installation locations
223+
// This helps when running in desktop apps (like Claude Desktop) that don't have full PATH
224+
const commonLocations = process.platform === 'win32'
225+
? [
226+
'C:\\Program Files\\qsv\\qsv.exe',
227+
'C:\\qsv\\qsv.exe',
228+
join(homedir(), 'scoop', 'shims', 'qsv.exe'),
229+
join(homedir(), 'AppData', 'Local', 'Programs', 'qsv', 'qsv.exe'),
230+
]
231+
: [
232+
'/usr/local/bin/qsv',
233+
'/opt/homebrew/bin/qsv', // Apple Silicon homebrew
234+
'/usr/bin/qsv',
235+
join(homedir(), '.cargo', 'bin', 'qsv'),
236+
join(homedir(), '.local', 'bin', 'qsv'),
237+
];
238+
239+
// Try each common location
240+
for (const location of commonLocations) {
241+
const diagnostic: any = { path: location, exists: false };
242+
243+
try {
244+
// Check if file exists and is executable
245+
const stats = statSync(location);
246+
diagnostic.exists = true;
247+
diagnostic.isFile = stats.isFile();
248+
249+
if (stats.isFile()) {
250+
// Verify it's actually qsv by trying to run it
251+
try {
252+
const versionOutput = execFileSync(location, ['--version'], {
253+
encoding: 'utf8',
254+
stdio: ['ignore', 'pipe', 'ignore'],
255+
timeout: QSV_VALIDATION_TIMEOUT_MS
256+
});
257+
diagnostic.executable = true;
258+
diagnostic.version = versionOutput.trim().split('\n')[0];
259+
lastDetectionDiagnostics.locationsChecked.push(diagnostic);
260+
return location; // Found it!
261+
} catch (execError) {
262+
diagnostic.executable = false;
263+
diagnostic.error = execError instanceof Error ? execError.message : String(execError);
264+
}
265+
}
266+
} catch (statError) {
267+
diagnostic.error = statError instanceof Error ? statError.message : String(statError);
268+
}
269+
270+
lastDetectionDiagnostics.locationsChecked.push(diagnostic);
271+
}
272+
273+
return null;
185274
}
186275

187276
/**

.claude/skills/src/mcp-server.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ import {
2626
createGenericToolDefinition,
2727
handleToolCall,
2828
handleGenericCommand,
29+
createConfigTool,
2930
createWelcomeTool,
3031
createExamplesTool,
32+
handleConfigTool,
3133
handleWelcomeTool,
3234
handleExamplesTool,
3335
initiateShutdown,
@@ -272,12 +274,13 @@ class QsvMcpServer {
272274
throw error;
273275
}
274276

275-
// Add welcome and examples tools
276-
console.error('[Server] Adding welcome and examples tools...');
277+
// Add welcome, config, and examples tools
278+
console.error('[Server] Adding welcome, config, and examples tools...');
277279
try {
278280
tools.push(createWelcomeTool());
281+
tools.push(createConfigTool());
279282
tools.push(createExamplesTool());
280-
console.error('[Server] Welcome and examples tools added successfully');
283+
console.error('[Server] Welcome, config, and examples tools added successfully');
281284
} catch (error) {
282285
console.error('[Server] Error creating welcome/examples tools:', error);
283286
throw error;
@@ -411,6 +414,11 @@ class QsvMcpServer {
411414
return await handleWelcomeTool(this.filesystemProvider);
412415
}
413416

417+
// Handle config tool
418+
if (name === 'qsv_config') {
419+
return await handleConfigTool(this.filesystemProvider);
420+
}
421+
414422
// Handle examples tool
415423
if (name === 'qsv_examples') {
416424
return await handleExamplesTool();

.claude/skills/src/mcp-tools.ts

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ConvertedFileManager } from './converted-file-manager.js';
1010
import type { QsvSkill, Argument, Option, McpToolDefinition, McpToolProperty, FilesystemProviderExtended } from './types.js';
1111
import type { SkillExecutor } from './executor.js';
1212
import type { SkillLoader } from './loader.js';
13-
import { config } from './config.js';
13+
import { config, getDetectionDiagnostics } from './config.js';
1414
import { formatBytes, findSimilarFiles } from './utils.js';
1515

1616
/**
@@ -884,6 +884,21 @@ export function createGenericToolDefinition(loader: SkillLoader): McpToolDefinit
884884
};
885885
}
886886

887+
/**
888+
* Create qsv_config tool definition
889+
*/
890+
export function createConfigTool(): McpToolDefinition {
891+
return {
892+
name: 'qsv_config',
893+
description: 'Display current qsv configuration (binary path, version, working directory, etc.)',
894+
inputSchema: {
895+
type: 'object',
896+
properties: {},
897+
required: [],
898+
},
899+
};
900+
}
901+
887902
/**
888903
* Create qsv_welcome tool definition
889904
*/
@@ -1038,6 +1053,132 @@ Ready to start wrangling data? 🚀`;
10381053
};
10391054
}
10401055

1056+
/**
1057+
* Handle qsv_config tool call
1058+
*/
1059+
export async function handleConfigTool(filesystemProvider?: FilesystemProviderExtended): Promise<{ content: Array<{ type: string; text: string }> }> {
1060+
const validation = config.qsvValidation;
1061+
const extensionMode = config.isExtensionMode;
1062+
1063+
let configText = `# qsv Configuration\n\n`;
1064+
1065+
// qsv Binary Information
1066+
configText += `## qsv Binary\n\n`;
1067+
if (validation.valid) {
1068+
configText += `✅ **Status:** Validated\n`;
1069+
configText += `📍 **Path:** \`${validation.path}\`\n`;
1070+
configText += `🏷️ **Version:** ${validation.version}\n`;
1071+
} else {
1072+
configText += `❌ **Status:** Validation Failed\n`;
1073+
configText += `⚠️ **Error:** ${validation.error}\n`;
1074+
1075+
// Show auto-detection diagnostics
1076+
const diagnostics = getDetectionDiagnostics();
1077+
if (diagnostics.whichAttempted) {
1078+
configText += `\n### 🔍 Auto-Detection Diagnostics\n\n`;
1079+
1080+
// Show which/where attempt
1081+
configText += `**PATH search (which/where):**\n`;
1082+
if (diagnostics.whichResult) {
1083+
configText += `✅ Found: \`${diagnostics.whichResult}\`\n\n`;
1084+
} else if (diagnostics.whichError) {
1085+
configText += `❌ Failed: ${diagnostics.whichError}\n\n`;
1086+
} else {
1087+
configText += `❌ Not found in PATH\n\n`;
1088+
}
1089+
1090+
// Show common locations checked
1091+
if (diagnostics.locationsChecked.length > 0) {
1092+
configText += `**Common locations checked:**\n\n`;
1093+
diagnostics.locationsChecked.forEach((loc) => {
1094+
configText += `- \`${loc.path}\`\n`;
1095+
if (loc.exists) {
1096+
configText += ` - ✅ File exists\n`;
1097+
if (loc.isFile !== undefined) {
1098+
configText += ` - ${loc.isFile ? '✅' : '❌'} Is regular file: ${loc.isFile}\n`;
1099+
}
1100+
if (loc.executable !== undefined) {
1101+
configText += ` - ${loc.executable ? '✅' : '❌'} Executable: ${loc.executable}\n`;
1102+
}
1103+
if (loc.version) {
1104+
configText += ` - ✅ Version: ${loc.version}\n`;
1105+
}
1106+
if (loc.error) {
1107+
configText += ` - ⚠️ Error: ${loc.error}\n`;
1108+
}
1109+
} else {
1110+
configText += ` - ❌ Does not exist\n`;
1111+
if (loc.error) {
1112+
configText += ` - ⚠️ Error: ${loc.error}\n`;
1113+
}
1114+
}
1115+
});
1116+
configText += `\n`;
1117+
}
1118+
}
1119+
}
1120+
1121+
// Working Directory
1122+
configText += `\n## Working Directory\n\n`;
1123+
if (filesystemProvider) {
1124+
const workingDir = filesystemProvider.getWorkingDirectory();
1125+
configText += `📁 **Current:** \`${workingDir}\`\n`;
1126+
} else {
1127+
configText += `📁 **Current:** \`${config.workingDir}\`\n`;
1128+
}
1129+
1130+
// Allowed Directories
1131+
configText += `\n## Allowed Directories\n\n`;
1132+
if (config.allowedDirs.length > 0) {
1133+
configText += `🔓 **Access granted to:**\n`;
1134+
config.allowedDirs.forEach(dir => {
1135+
configText += ` - \`${dir}\`\n`;
1136+
});
1137+
} else {
1138+
configText += `ℹ️ Only working directory is accessible\n`;
1139+
}
1140+
1141+
// Performance Settings
1142+
configText += `\n## Performance Settings\n\n`;
1143+
configText += `⏱️ **Timeout:** ${config.timeoutMs}ms (${Math.round(config.timeoutMs / 1000)}s)\n`;
1144+
configText += `💾 **Max Output Size:** ${formatBytes(config.maxOutputSize)}\n`;
1145+
configText += `🔧 **Auto-Regenerate Skills:** ${config.autoRegenerateSkills ? 'Enabled' : 'Disabled'}\n`;
1146+
1147+
// Update Check Settings
1148+
configText += `\n## Update Settings\n\n`;
1149+
configText += `🔍 **Check Updates on Startup:** ${config.checkUpdatesOnStartup ? 'Enabled' : 'Disabled'}\n`;
1150+
configText += `📢 **Update Notifications:** ${config.notifyUpdates ? 'Enabled' : 'Disabled'}\n`;
1151+
1152+
// Mode
1153+
configText += `\n## Deployment Mode\n\n`;
1154+
configText += `${extensionMode ? '🧩 **Desktop Extension Mode**' : '🖥️ **Legacy MCP Server Mode**'}\n`;
1155+
1156+
// Help Text
1157+
configText += `\n---\n\n`;
1158+
if (!validation.valid) {
1159+
configText += `### ⚠️ Action Required\n\n`;
1160+
if (extensionMode) {
1161+
configText += `To fix the qsv binary issue:\n`;
1162+
configText += `1. Install qsv from https://github.com/dathere/qsv#installation\n`;
1163+
configText += `2. Open Claude Desktop Settings > Extensions > qsv\n`;
1164+
configText += `3. Update "qsv Binary Path" or ensure qsv is in your system PATH\n`;
1165+
configText += `4. Save settings (extension will auto-restart)\n`;
1166+
} else {
1167+
configText += `To fix the qsv binary issue:\n`;
1168+
configText += `1. Install qsv from https://github.com/dathere/qsv#installation\n`;
1169+
configText += `2. Ensure qsv is in your PATH or set QSV_MCP_BIN_PATH\n`;
1170+
configText += `3. Restart the MCP server\n`;
1171+
}
1172+
} else {
1173+
configText += `### 💡 Tip\n\n`;
1174+
configText += `These are the actual resolved values used by the server. The configuration UI may show template variables like \`\${HOME}/Downloads\` which get expanded to the paths shown above.\n`;
1175+
}
1176+
1177+
return {
1178+
content: [{ type: 'text', text: configText }],
1179+
};
1180+
}
1181+
10411182
/**
10421183
* Handle qsv_examples tool call
10431184
*/

0 commit comments

Comments
 (0)