@probelabs/probe 0.6.0-rc56

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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Default system message for code intelligence assistants
3
+ * @module tools/system-message
4
+ */
5
+ export const DEFAULT_SYSTEM_MESSAGE = `[Persona & Objective]
6
+
7
+ You are Probe, a specialized code intelligence assistant. Your objective is to accurately answer questions about multi-language codebases by effectively using your available tools: \`search\`, \`query\`, and \`extract\`.
8
+
9
+ [Core Workflow & Principles]
10
+
11
+ 1. **Tool-First Always:** Immediately use tools for any code-related query. Do not guess or use general knowledge.
12
+ 2. **Mandatory Path:** ALL tool calls (\`search\`, \`query\`, \`extract\`) MUST include the \`path\` argument. Use \`"."\` for the whole project, specific directories/files (e.g., \`"src/api"\`, \`"pkg/utils/helpers.py"\`), or dependency syntax (e.g., \`"go:github.com/gin-gonic/gin"\`, \`"js:@ai-sdk/anthropic"\`, \`"rust:serde"\`).
13
+ 3. **Start with \`search\`:**
14
+ * **Keywords are Key:** Formulate queries like you would in Elasticsearch. Use specific keywords, boolean operators (\`AND\`, \`OR\`, \`NOT\`), and exact phrases (\`""\`). This is NOT a simple text search.
15
+ * **Iterate if Needed:** If initial results are too broad or insufficient, **repeat the exact same \`search\` query** to get the next page of results (pagination). Reuse the \`sessionID\` if provided by the previous identical search. If results are irrelevant, refine the keywords (add terms, use \`NOT\`, try synonyms).
16
+ 4. **Analyze & Refine:** Review \`search\` results (snippets, file paths).
17
+ * Use \`query\` if you need code based on *structure* (AST patterns) within specific files/directories identified by \`search\`.
18
+ * Use \`extract\` if \`search\` or \`query\` identified the exact location (file, symbol, line range) and you need the full definition or more context.
19
+ 5. **Synthesize & Cite:** Construct the answer *only* from tool outputs. ALWAYS cite the specific file paths and relevant locations (symbols, line numbers) found. Adapt detail to the likely user role (developer vs. PM).
20
+ 6. **Clarify Sparingly:** If an initial \`search\` attempt completely fails due to ambiguity, ask a *specific* question to guide the next search. Don't ask before trying a search first.
21
+
22
+ [Tool Reference]
23
+
24
+ * \`search\`
25
+ * **Purpose:** Find relevant code snippets/files using keyword-based search (like Elasticsearch). Locate named symbols. Search project code or dependencies.
26
+ * **Syntax:** \`query\` (Elasticsearch-like string: keywords, \`AND\`, \`OR\`, \`NOT\`, \`""\` exact phrases), \`path\` (Mandatory: \`"."\`, \`"path/to/dir"\`, \`"path/to/file.ext"\`, \`"go:pkg"\`, \`"js:npm_module"\`, \`"rust:crate"\`), \`exact\` (Optional: Set to \`true\` for case-insensitive exact matching without tokenization).
27
+ * **Features:** Returns snippets/paths. Supports pagination (repeat query). Caching via \`sessionID\` (reuse if returned). Use \`exact\` flag when you need precise matching of terms.
28
+ * \`query\`
29
+ * **Purpose:** Find code by its *structure* (AST patterns) within specific files/directories, typically after \`search\`.
30
+ * **Syntax:** \`pattern\` (ast-grep pattern), \`language\` (e.g., "go", "python").
31
+ * **Mandatory Argument:** \`path\` (file or directory path, e.g., \`"src/services"\`, \`"app/main.py"\`).
32
+ * \`extract\`
33
+ * **Purpose:** Retrieve specific code blocks or entire files *after* \`search\` or \`query\` identifies the target.
34
+ * **Syntax:** Optional \`#symbol\` (e.g., \`#MyClass\`), \`#Lstart-Lend\` (e.g., \`#L50-L75\`).
35
+ * **Mandatory Argument:** \`path\` (specific file path, e.g., \`"src/utils/helpers.go"\`, or dependency file like \`"go:github.com/gin-gonic/gin/context.go"\`).
36
+
37
+ [Examples]
38
+
39
+ * **Example 1: Finding a Specific Function Definition**
40
+ * User: "Show me the code for the \`calculate_total\` function in our payments module."
41
+ * Probe Action 1: \`search\` query: \`"calculate_total"\`, path: \`"src/payments"\` (Targeted search in the likely directory)
42
+ * (Analysis: Search returns a clear hit in \`src/payments/logic.py\`.)
43
+ * Probe Action 2: \`extract\` path: \`"src/payments/logic.py#calculate_total"\`
44
+ * (Response: Provide the extracted function code, citing \`src/payments/logic.py#calculate_total\`.)
45
+
46
+ * **Example 2: Investigating Initialization**
47
+ * User: "Where is the primary configuration for the Redis cache loaded?"
48
+ * Probe Action 1: \`search\` query: \`redis AND (config OR load OR init OR setup) NOT test\`, path: \`"."\`
49
+ * (Analysis: Results point towards \`pkg/cache/redis.go\` and a function \`LoadRedisConfig\`.)
50
+ * Probe Action 2: \`extract\` path: \`"pkg/cache/redis.go#LoadRedisConfig"\`
51
+ * (Response: Explain config loading based on the extracted \`LoadRedisConfig\` function, citing \`pkg/cache/redis.go#LoadRedisConfig\`.)
52
+
53
+ * **Example 3: Understanding Usage of a Dependency Feature**
54
+ * User: "How are we using the \`createAnthropic\` function from the \`@ai-sdk/anthropic\` library?"
55
+ * Probe Action 1: \`search\` query: \`"createAnthropic"\`, path: \`"."\` (Search project code for usage)
56
+ * (Analysis: Find usage in \`src/ai/providers.ts\`. Want to understand the library function itself better.)
57
+ * Probe Action 2: \`search\` query: \`"createAnthropic"\`, path: \`"js:@ai-sdk/anthropic"\` (Search within the specific dependency)
58
+ * (Analysis: Search locates the definition within the dependency code, e.g., \`node_modules/@ai-sdk/anthropic/dist/index.js\` or similar mapped path.)
59
+ * Probe Action 3: \`extract\` path: \`"js:@ai-sdk/anthropic/dist/index.js#createAnthropic"\` (Extract the specific function *from the dependency*. Note: Actual file path within dependency might vary, use the one found by search).
60
+ * (Response: Show how \`createAnthropic\` is used in \`src/ai/providers.ts\`, and explain its purpose based on the extracted definition from the \`@ai-sdk/anthropic\` library, citing both files.)
61
+
62
+ * **Example 4: Exploring Error Handling Patterns**
63
+ * User: "What's the standard way errors are wrapped or handled in our Go backend services?"
64
+ * Probe Action 1: \`search\` query: \`error AND (wrap OR handle OR new) AND lang:go NOT test\`, path: \`"service/"\` (Focus on service directories)
65
+ * (Analysis: Many results. See frequent use of \`fmt.Errorf\` and a custom \`errors.Wrap\` in several files like \`service/user/handler.go\`.)
66
+ * Probe Action 2: \`search\` query: \`import AND "pkg/errors"\`, path: \`"service/"\` (Check where a potential custom error package is used)
67
+ * (Analysis: Confirms \`pkg/errors\` is widely used.)
68
+ * Probe Action 3: \`query\` language: \`go\`, pattern: \`errors.Wrap($$$)\`, path: \`"service/"\` (Find structural usage of the custom wrapper)
69
+ * (Response: Summarize error handling: Mention standard \`fmt.Errorf\` and the prevalent use of a custom \`errors.Wrap\` function from \`pkg/errors\`, providing examples from locations found by search/query like \`service/user/handler.go\`.)`
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Tools for Vercel AI SDK
3
+ * @module tools/vercel
4
+ */
5
+
6
+ import { tool } from 'ai';
7
+ import { search } from '../search.js';
8
+ import { query } from '../query.js';
9
+ import { extract } from '../extract.js';
10
+ import { searchSchema, querySchema, extractSchema, searchDescription, queryDescription, extractDescription } from './common.js';
11
+
12
+ /**
13
+ * Search tool generator
14
+ *
15
+ * @param {Object} [options] - Configuration options
16
+ * @param {string} [options.sessionId] - Session ID for caching search results
17
+ * @param {number} [options.maxTokens=10000] - Default max tokens
18
+ * @param {boolean} [options.debug=false] - Enable debug logging
19
+ * @returns {Object} Configured search tool
20
+ */
21
+ export const searchTool = (options = {}) => {
22
+ const { sessionId, maxTokens = 10000, debug = false } = options;
23
+
24
+ return tool({
25
+ name: 'search',
26
+ description: searchDescription,
27
+ parameters: searchSchema,
28
+ execute: async ({ query: searchQuery, path, allow_tests, exact, maxTokens: paramMaxTokens, language }) => {
29
+ try {
30
+ // Use parameter maxTokens if provided, otherwise use the default
31
+ const effectiveMaxTokens = paramMaxTokens || maxTokens;
32
+
33
+ // Use the path from parameters if provided, otherwise use defaultPath from config
34
+ let searchPath = path || options.defaultPath || '.';
35
+
36
+ // If path is "." or "./", use the defaultPath if available
37
+ if ((searchPath === "." || searchPath === "./") && options.defaultPath) {
38
+ if (debug) {
39
+ console.error(`Using default path "${options.defaultPath}" instead of "${searchPath}"`);
40
+ }
41
+ searchPath = options.defaultPath;
42
+ }
43
+
44
+ if (debug) {
45
+ console.error(`Executing search with query: "${searchQuery}", path: "${searchPath}", exact: ${exact ? 'true' : 'false'}, language: ${language || 'all'}, session: ${sessionId || 'none'}`);
46
+ }
47
+
48
+ const results = await search({
49
+ query: searchQuery,
50
+ path: searchPath,
51
+ allow_tests,
52
+ exact,
53
+ json: false,
54
+ maxTokens: effectiveMaxTokens,
55
+ session: sessionId, // Pass session ID if provided
56
+ language // Pass language parameter if provided
57
+ });
58
+
59
+ return results;
60
+ } catch (error) {
61
+ console.error('Error executing search command:', error);
62
+ return `Error executing search command: ${error.message}`;
63
+ }
64
+ }
65
+ });
66
+ };
67
+
68
+ /**
69
+ * Query tool generator
70
+ *
71
+ * @param {Object} [options] - Configuration options
72
+ * @param {boolean} [options.debug=false] - Enable debug logging
73
+ * @returns {Object} Configured query tool
74
+ */
75
+ export const queryTool = (options = {}) => {
76
+ const { debug = false } = options;
77
+
78
+ return tool({
79
+ name: 'query',
80
+ description: queryDescription,
81
+ parameters: querySchema,
82
+ execute: async ({ pattern, path, language, allow_tests }) => {
83
+ try {
84
+ // Use the path from parameters if provided, otherwise use defaultPath from config
85
+ let queryPath = path || options.defaultPath || '.';
86
+
87
+ // If path is "." or "./", use the defaultPath if available
88
+ if ((queryPath === "." || queryPath === "./") && options.defaultPath) {
89
+ if (debug) {
90
+ console.error(`Using default path "${options.defaultPath}" instead of "${queryPath}"`);
91
+ }
92
+ queryPath = options.defaultPath;
93
+ }
94
+
95
+ if (debug) {
96
+ console.error(`Executing query with pattern: "${pattern}", path: "${queryPath}", language: ${language || 'auto'}`);
97
+ }
98
+
99
+ const results = await query({
100
+ pattern,
101
+ path: queryPath,
102
+ language,
103
+ allow_tests,
104
+ json: false
105
+ });
106
+
107
+ return results;
108
+ } catch (error) {
109
+ console.error('Error executing query command:', error);
110
+ return `Error executing query command: ${error.message}`;
111
+ }
112
+ }
113
+ });
114
+ };
115
+
116
+ /**
117
+ * Extract tool generator
118
+ *
119
+ * @param {Object} [options] - Configuration options
120
+ * @param {boolean} [options.debug=false] - Enable debug logging
121
+ * @returns {Object} Configured extract tool
122
+ */
123
+ export const extractTool = (options = {}) => {
124
+ const { debug = false } = options;
125
+
126
+ return tool({
127
+ name: 'extract',
128
+ description: extractDescription,
129
+ parameters: extractSchema,
130
+ execute: async ({ file_path, input_content, line, end_line, allow_tests, context_lines, format }) => {
131
+ try {
132
+ // Use the defaultPath from config for context
133
+ let extractPath = options.defaultPath || '.';
134
+
135
+ // If path is "." or "./", use the defaultPath if available
136
+ if ((extractPath === "." || extractPath === "./") && options.defaultPath) {
137
+ if (debug) {
138
+ console.error(`Using default path "${options.defaultPath}" instead of "${extractPath}"`);
139
+ }
140
+ extractPath = options.defaultPath;
141
+ }
142
+
143
+ if (debug) {
144
+ if (file_path) {
145
+ console.error(`Executing extract with file: "${file_path}", path: "${extractPath}", context lines: ${context_lines || 10}`);
146
+ } else if (input_content) {
147
+ console.error(`Executing extract with input content, path: "${extractPath}", context lines: ${context_lines || 10}`);
148
+ }
149
+ }
150
+
151
+ // Create a temporary file for input content if provided
152
+ let tempFilePath = null;
153
+ let extractOptions = { path: extractPath };
154
+
155
+ if (input_content) {
156
+ // Import required modules
157
+ const { writeFileSync, unlinkSync } = await import('fs');
158
+ const { join } = await import('path');
159
+ const { tmpdir } = await import('os');
160
+ const { randomUUID } = await import('crypto');
161
+
162
+ // Create a temporary file with the input content
163
+ tempFilePath = join(tmpdir(), `probe-extract-${randomUUID()}.txt`);
164
+ writeFileSync(tempFilePath, input_content);
165
+
166
+ if (debug) {
167
+ console.error(`Created temporary file for input content: ${tempFilePath}`);
168
+ }
169
+
170
+ // Set up extract options with input file
171
+ extractOptions = {
172
+ inputFile: tempFilePath,
173
+ allowTests: allow_tests,
174
+ contextLines: context_lines,
175
+ format
176
+ };
177
+ } else if (file_path) {
178
+ // Parse file_path to handle line numbers and symbol names
179
+ const files = [file_path];
180
+
181
+ // Set up extract options with files
182
+ extractOptions = {
183
+ files,
184
+ allowTests: allow_tests,
185
+ contextLines: context_lines,
186
+ format
187
+ };
188
+ } else {
189
+ throw new Error('Either file_path or input_content must be provided');
190
+ }
191
+
192
+ // Execute the extract command
193
+ const results = await extract(extractOptions);
194
+
195
+ // Clean up temporary file if created
196
+ if (tempFilePath) {
197
+ const { unlinkSync } = await import('fs');
198
+ try {
199
+ unlinkSync(tempFilePath);
200
+ if (debug) {
201
+ console.error(`Removed temporary file: ${tempFilePath}`);
202
+ }
203
+ } catch (cleanupError) {
204
+ console.error(`Warning: Failed to remove temporary file: ${cleanupError.message}`);
205
+ }
206
+ }
207
+
208
+ return results;
209
+ } catch (error) {
210
+ console.error('Error executing extract command:', error);
211
+ return `Error executing extract command: ${error.message}`;
212
+ }
213
+ }
214
+ });
215
+ };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * File listing utility for the probe package
3
+ * @module utils/file-lister
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { promisify } from 'util';
9
+ import { exec } from 'child_process';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ /**
14
+ * List files in a directory by nesting level, respecting .gitignore
15
+ *
16
+ * @param {Object} options - Options for listing files
17
+ * @param {string} options.directory - Directory to list files from
18
+ * @param {number} [options.maxFiles=100] - Maximum number of files to return
19
+ * @param {boolean} [options.respectGitignore=true] - Whether to respect .gitignore
20
+ * @returns {Promise<string[]>} - Array of file paths
21
+ */
22
+ export async function listFilesByLevel(options) {
23
+ const {
24
+ directory,
25
+ maxFiles = 100,
26
+ respectGitignore = true
27
+ } = options;
28
+
29
+ // Check if directory exists
30
+ if (!fs.existsSync(directory)) {
31
+ throw new Error(`Directory does not exist: ${directory}`);
32
+ }
33
+
34
+ // Use git ls-files if .git directory exists and respectGitignore is true
35
+ const gitDirExists = fs.existsSync(path.join(directory, '.git'));
36
+ if (gitDirExists && respectGitignore) {
37
+ try {
38
+ return await listFilesUsingGit(directory, maxFiles);
39
+ } catch (error) {
40
+ console.error(`Warning: Failed to use git ls-files: ${error.message}`);
41
+ console.error('Falling back to manual file listing');
42
+ }
43
+ }
44
+
45
+ // Fall back to manual file listing
46
+ return await listFilesByLevelManually(directory, maxFiles, respectGitignore);
47
+ }
48
+
49
+ /**
50
+ * List files using git ls-files (respects .gitignore by default)
51
+ *
52
+ * @param {string} directory - Directory to list files from
53
+ * @param {number} maxFiles - Maximum number of files to return
54
+ * @returns {Promise<string[]>} - Array of file paths
55
+ */
56
+ async function listFilesUsingGit(directory, maxFiles) {
57
+ // Use git ls-files to get tracked files (respects .gitignore)
58
+ const { stdout } = await execAsync('git ls-files', { cwd: directory });
59
+
60
+ // Split output into lines and filter out empty lines
61
+ const files = stdout.split('\n').filter(Boolean);
62
+
63
+ // Sort files by directory depth (breadth-first)
64
+ const sortedFiles = files.sort((a, b) => {
65
+ const depthA = a.split(path.sep).length;
66
+ const depthB = b.split(path.sep).length;
67
+ return depthA - depthB;
68
+ });
69
+
70
+ // Limit to maxFiles
71
+ return sortedFiles.slice(0, maxFiles);
72
+ }
73
+
74
+ /**
75
+ * List files manually by nesting level
76
+ *
77
+ * @param {string} directory - Directory to list files from
78
+ * @param {number} maxFiles - Maximum number of files to return
79
+ * @param {boolean} respectGitignore - Whether to respect .gitignore
80
+ * @returns {Promise<string[]>} - Array of file paths
81
+ */
82
+ async function listFilesByLevelManually(directory, maxFiles, respectGitignore) {
83
+ // Load .gitignore patterns if needed
84
+ let ignorePatterns = [];
85
+ if (respectGitignore) {
86
+ ignorePatterns = loadGitignorePatterns(directory);
87
+ }
88
+
89
+ // Initialize result array
90
+ const result = [];
91
+
92
+ // Initialize queue with root directory
93
+ const queue = [{ dir: directory, level: 0 }];
94
+
95
+ // Process queue (breadth-first)
96
+ while (queue.length > 0 && result.length < maxFiles) {
97
+ const { dir, level } = queue.shift();
98
+
99
+ try {
100
+ // Read directory contents
101
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
102
+
103
+ // Process files first (at current level)
104
+ const files = entries.filter(entry => entry.isFile());
105
+ for (const file of files) {
106
+ if (result.length >= maxFiles) break;
107
+
108
+ const filePath = path.join(dir, file.name);
109
+ const relativePath = path.relative(directory, filePath);
110
+
111
+ // Skip if file matches any ignore pattern
112
+ if (shouldIgnore(relativePath, ignorePatterns)) continue;
113
+
114
+ result.push(relativePath);
115
+ }
116
+
117
+ // Then add directories to queue for next level
118
+ const dirs = entries.filter(entry => entry.isDirectory());
119
+ for (const subdir of dirs) {
120
+ const subdirPath = path.join(dir, subdir.name);
121
+ const relativeSubdirPath = path.relative(directory, subdirPath);
122
+
123
+ // Skip if directory matches any ignore pattern
124
+ if (shouldIgnore(relativeSubdirPath, ignorePatterns)) continue;
125
+
126
+ // Skip node_modules and .git directories
127
+ if (subdir.name === 'node_modules' || subdir.name === '.git') continue;
128
+
129
+ queue.push({ dir: subdirPath, level: level + 1 });
130
+ }
131
+ } catch (error) {
132
+ console.error(`Warning: Could not read directory ${dir}: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * Load .gitignore patterns from a directory
141
+ *
142
+ * @param {string} directory - Directory to load .gitignore from
143
+ * @returns {string[]} - Array of ignore patterns
144
+ */
145
+ function loadGitignorePatterns(directory) {
146
+ const gitignorePath = path.join(directory, '.gitignore');
147
+
148
+ if (!fs.existsSync(gitignorePath)) {
149
+ return [];
150
+ }
151
+
152
+ try {
153
+ const content = fs.readFileSync(gitignorePath, 'utf8');
154
+ return content
155
+ .split('\n')
156
+ .map(line => line.trim())
157
+ .filter(line => line && !line.startsWith('#'));
158
+ } catch (error) {
159
+ console.error(`Warning: Could not read .gitignore: ${error.message}`);
160
+ return [];
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Check if a file path should be ignored based on ignore patterns
166
+ *
167
+ * @param {string} filePath - File path to check
168
+ * @param {string[]} ignorePatterns - Array of ignore patterns
169
+ * @returns {boolean} - Whether the file should be ignored
170
+ */
171
+ function shouldIgnore(filePath, ignorePatterns) {
172
+ if (!ignorePatterns.length) return false;
173
+
174
+ // Simple pattern matching (could be improved with minimatch or similar)
175
+ for (const pattern of ignorePatterns) {
176
+ // Exact match
177
+ if (pattern === filePath) return true;
178
+
179
+ // Directory match (pattern ends with /)
180
+ if (pattern.endsWith('/') && filePath.startsWith(pattern)) return true;
181
+
182
+ // File extension match (pattern starts with *.)
183
+ if (pattern.startsWith('*.') && filePath.endsWith(pattern.substring(1))) return true;
184
+
185
+ // Wildcard at start (pattern starts with *)
186
+ if (pattern.startsWith('*') && filePath.endsWith(pattern.substring(1))) return true;
187
+
188
+ // Wildcard at end (pattern ends with *)
189
+ if (pattern.endsWith('*') && filePath.startsWith(pattern.substring(0, pattern.length - 1))) return true;
190
+ }
191
+
192
+ return false;
193
+ }
package/src/utils.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Utility functions for the probe package
3
+ * @module utils
4
+ */
5
+
6
+ import path from 'path';
7
+ import fs from 'fs-extra';
8
+ import { fileURLToPath } from 'url';
9
+ import { downloadProbeBinary } from './downloader.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const binDir = path.resolve(__dirname, '..', 'bin');
14
+
15
+ // Store the binary path
16
+ let probeBinaryPath = '';
17
+
18
+ /**
19
+ * Get the path to the probe binary, downloading it if necessary
20
+ * @param {Object} options - Options for getting the binary
21
+ * @param {boolean} [options.forceDownload=false] - Force download even if binary exists
22
+ * @param {string} [options.version] - Specific version to download
23
+ * @returns {Promise<string>} - Path to the binary
24
+ */
25
+ export async function getBinaryPath(options = {}) {
26
+ const { forceDownload = false, version } = options;
27
+
28
+ // Return cached path if available and not forcing download
29
+ if (probeBinaryPath && !forceDownload && fs.existsSync(probeBinaryPath)) {
30
+ return probeBinaryPath;
31
+ }
32
+
33
+ // Check environment variable
34
+ if (process.env.PROBE_PATH && fs.existsSync(process.env.PROBE_PATH) && !forceDownload) {
35
+ probeBinaryPath = process.env.PROBE_PATH;
36
+ return probeBinaryPath;
37
+ }
38
+
39
+ // Check bin directory
40
+ const isWindows = process.platform === 'win32';
41
+ const binaryName = isWindows ? 'probe.exe' : 'probe';
42
+ const binaryPath = path.join(binDir, binaryName);
43
+
44
+ if (fs.existsSync(binaryPath) && !forceDownload) {
45
+ probeBinaryPath = binaryPath;
46
+ return probeBinaryPath;
47
+ }
48
+
49
+ // Download if not found or force download
50
+ console.log(`${forceDownload ? 'Force downloading' : 'Binary not found. Downloading'} probe binary...`);
51
+ probeBinaryPath = await downloadProbeBinary(version);
52
+ return probeBinaryPath;
53
+ }
54
+
55
+ /**
56
+ * Manually set the path to the probe binary
57
+ * @param {string} binaryPath - Path to the probe binary
58
+ * @throws {Error} If the binary doesn't exist at the specified path
59
+ */
60
+ export function setBinaryPath(binaryPath) {
61
+ if (!fs.existsSync(binaryPath)) {
62
+ throw new Error(`No binary found at path: ${binaryPath}`);
63
+ }
64
+
65
+ probeBinaryPath = binaryPath;
66
+ }
67
+
68
+ /**
69
+ * Ensure the bin directory exists
70
+ * @returns {Promise<void>}
71
+ */
72
+ export async function ensureBinDirectory() {
73
+ await fs.ensureDir(binDir);
74
+ }
75
+
76
+ /**
77
+ * Build command-line arguments from an options object
78
+ * @param {Object} options - Options object
79
+ * @param {Array<string>} flagMap - Map of option keys to command-line flags
80
+ * @returns {Array<string>} - Array of command-line arguments
81
+ */
82
+ export function buildCliArgs(options, flagMap) {
83
+ const cliArgs = [];
84
+
85
+ for (const [key, flag] of Object.entries(flagMap)) {
86
+ if (key in options) {
87
+ const value = options[key];
88
+
89
+ if (typeof value === 'boolean') {
90
+ if (value) {
91
+ cliArgs.push(flag);
92
+ }
93
+ } else if (Array.isArray(value)) {
94
+ for (const item of value) {
95
+ cliArgs.push(flag, item);
96
+ }
97
+ } else if (value !== undefined && value !== null) {
98
+ cliArgs.push(flag, value.toString());
99
+ }
100
+ }
101
+ }
102
+
103
+ return cliArgs;
104
+ }
105
+
106
+ /**
107
+ * Escape a string for use in a command line
108
+ * @param {string} str - String to escape
109
+ * @returns {string} - Escaped string
110
+ */
111
+ export function escapeString(str) {
112
+ if (process.platform === 'win32') {
113
+ // For Windows PowerShell, escape double quotes and wrap with double quotes
114
+ return `"${str.replace(/"/g, '\\"')}"`;
115
+ } else {
116
+ // Use single quotes for POSIX shells
117
+ // Escape single quotes in the string by replacing ' with '\''
118
+ return `'${str.replace(/'/g, "'\\''")}'`;
119
+ }
120
+ }