@probelabs/probe 0.6.0-rc142 → 0.6.0-rc143

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.
@@ -29,6 +29,230 @@ const __dirname = path.dirname(__filename);
29
29
  // Note: LOCAL_DIR and VERSION_INFO_PATH are now resolved dynamically
30
30
  // using getPackageBinDir() to handle different installation environments
31
31
 
32
+ // Download lock management - prevents concurrent downloads
33
+ //
34
+ // Two-tier locking system:
35
+ // 1. In-memory locks: Prevent duplicate downloads within the same Node.js process
36
+ // 2. File-based locks: Coordinate downloads across separate processes
37
+ //
38
+ // How it works with multiple processes:
39
+ // Process A: Creates lock file → Downloads binary → Removes lock file
40
+ // Process B: Sees lock file → Polls every 1s → Binary appears → Uses binary
41
+ // Process C: Sees lock file → Polls every 1s → Binary appears → Uses binary
42
+ //
43
+ // The polling loop checks every second for:
44
+ // - Is the binary now available? (download completed)
45
+ // - Has the lock expired? (>5 minutes old, process crashed)
46
+ //
47
+ const downloadLocks = new Map(); // Map of version -> { promise, timestamp } (in-memory, per-process)
48
+ const LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes timeout for stuck downloads
49
+ const LOCK_POLL_INTERVAL_MS = 1000; // Poll every 1 second when waiting for file lock
50
+ const MAX_LOCK_WAIT_MS = 5 * 60 * 1000; // Maximum 5 minutes to wait for file lock
51
+
52
+ /**
53
+ * Acquires a file-based lock that works across processes
54
+ * @param {string} lockPath - Path to the lock file
55
+ * @param {string} version - Version being locked
56
+ * @returns {Promise<boolean|null>} True if lock was acquired, false if locked by another process, null if locking unavailable (permissions/errors)
57
+ */
58
+ async function acquireFileLock(lockPath, version) {
59
+ const lockData = {
60
+ version,
61
+ pid: process.pid,
62
+ timestamp: Date.now()
63
+ };
64
+
65
+ try {
66
+ // Try to create lock file atomically (fails if already exists)
67
+ await fs.writeFile(lockPath, JSON.stringify(lockData), { flag: 'wx' });
68
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
69
+ console.log(`Acquired file lock: ${lockPath}`);
70
+ }
71
+ return true;
72
+ } catch (error) {
73
+ if (error.code === 'EEXIST') {
74
+ // Lock file exists - check if it's stale
75
+ try {
76
+ const existingLock = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
77
+ const lockAge = Date.now() - existingLock.timestamp;
78
+
79
+ if (lockAge > LOCK_TIMEOUT_MS) {
80
+ // Lock is stale, remove it
81
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
82
+ console.log(`Removing stale lock file (age: ${Math.round(lockAge / 1000)}s, pid: ${existingLock.pid})`);
83
+ }
84
+ await fs.remove(lockPath);
85
+ return false; // Caller should retry
86
+ }
87
+
88
+ // Lock is fresh, another process is downloading
89
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
90
+ console.log(`Download in progress by process ${existingLock.pid}, waiting...`);
91
+ }
92
+ return false;
93
+ } catch (readError) {
94
+ // Can't read lock file, might be corrupted - remove it
95
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
96
+ console.log(`Lock file corrupted, removing: ${readError.message}`);
97
+ }
98
+ try {
99
+ await fs.remove(lockPath);
100
+ } catch {}
101
+ return false;
102
+ }
103
+ }
104
+
105
+ // Handle permission errors and other filesystem errors
106
+ if (error.code === 'EACCES' || error.code === 'EPERM' || error.code === 'EROFS') {
107
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
108
+ console.log(`Cannot create lock file (${error.code}): ${lockPath}`);
109
+ console.log(`File-based locking unavailable, will proceed without cross-process coordination`);
110
+ }
111
+ return null; // Lock unavailable, caller should proceed without it
112
+ }
113
+
114
+ // For other errors, log and return null (proceed without lock)
115
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
116
+ console.log(`Unexpected error creating lock file: ${error.message}`);
117
+ console.log(`Proceeding without file-based lock`);
118
+ }
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Releases a file-based lock
125
+ * @param {string} lockPath - Path to the lock file
126
+ */
127
+ async function releaseFileLock(lockPath) {
128
+ try {
129
+ await fs.remove(lockPath);
130
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
131
+ console.log(`Released file lock: ${lockPath}`);
132
+ }
133
+ } catch (error) {
134
+ // Ignore errors when releasing lock
135
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
136
+ console.log(`Warning: Could not release lock file: ${error.message}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Waits for a file-based lock to be released and the download to complete
143
+ * Uses a polling loop that checks every second for:
144
+ * 1. Binary is now available (download completed)
145
+ * 2. Lock has expired (>5 minutes old)
146
+ *
147
+ * @param {string} lockPath - Path to the lock file
148
+ * @param {string} binaryPath - Expected path to the downloaded binary
149
+ * @returns {Promise<boolean>} True if binary appeared, false if timed out
150
+ */
151
+ async function waitForFileLock(lockPath, binaryPath) {
152
+ const startTime = Date.now();
153
+
154
+ // Poll in a loop until binary appears, lock expires, or we timeout
155
+ while (Date.now() - startTime < MAX_LOCK_WAIT_MS) {
156
+ // Check #1: Is the binary now available?
157
+ if (await fs.pathExists(binaryPath)) {
158
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
159
+ console.log(`Binary now available at ${binaryPath}, download completed by another process`);
160
+ }
161
+ return true;
162
+ }
163
+
164
+ // Check #2: Is the lock file gone? (download finished or failed)
165
+ const lockExists = await fs.pathExists(lockPath);
166
+ if (!lockExists) {
167
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
168
+ console.log(`Lock file removed but binary not found - download may have failed`);
169
+ }
170
+ return false;
171
+ }
172
+
173
+ // Check #3: Is the lock stale (expired)?
174
+ try {
175
+ const lockData = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
176
+ const lockAge = Date.now() - lockData.timestamp;
177
+ if (lockAge > LOCK_TIMEOUT_MS) {
178
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
179
+ console.log(`Lock expired (age: ${Math.round(lockAge / 1000)}s), will retry download`);
180
+ }
181
+ return false;
182
+ }
183
+ } catch {
184
+ // Ignore errors reading lock file - will retry on next poll
185
+ }
186
+
187
+ // Wait 1 second before checking again
188
+ await new Promise(resolve => setTimeout(resolve, LOCK_POLL_INTERVAL_MS));
189
+ }
190
+
191
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
192
+ console.log(`Timeout waiting for file lock`);
193
+ }
194
+ return false;
195
+ }
196
+
197
+ /**
198
+ * Acquires a download lock for a specific version (in-memory for same process)
199
+ * If another download is in progress in the same process, waits for it to complete
200
+ * Includes timeout mechanism to prevent permanent locks from failed downloads
201
+ * @param {string} version - Version being downloaded
202
+ * @param {Function} downloadFn - Function to execute if lock is acquired
203
+ * @returns {Promise<string>} Path to the binary
204
+ */
205
+ async function withDownloadLock(version, downloadFn) {
206
+ const lockKey = version || 'latest';
207
+
208
+ // First, check in-memory lock (same process)
209
+ if (downloadLocks.has(lockKey)) {
210
+ const lock = downloadLocks.get(lockKey);
211
+ const lockAge = Date.now() - lock.timestamp;
212
+
213
+ // If lock is too old, it's likely stuck - remove it and start fresh
214
+ if (lockAge > LOCK_TIMEOUT_MS) {
215
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
216
+ console.log(`In-memory lock for version ${lockKey} expired (age: ${Math.round(lockAge / 1000)}s), removing stale lock`);
217
+ }
218
+ downloadLocks.delete(lockKey);
219
+ } else {
220
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
221
+ console.log(`Download already in progress in this process for version ${lockKey}, waiting...`);
222
+ }
223
+ try {
224
+ return await lock.promise;
225
+ } catch (error) {
226
+ // If the locked download failed, we'll try again below
227
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
228
+ console.log(`In-memory locked download failed, will retry: ${error.message}`);
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ // Create new download promise with timeout protection
235
+ const downloadPromise = Promise.race([
236
+ downloadFn(),
237
+ new Promise((_, reject) =>
238
+ setTimeout(() => reject(new Error(`Download timeout after ${LOCK_TIMEOUT_MS / 1000}s`)), LOCK_TIMEOUT_MS)
239
+ )
240
+ ]);
241
+
242
+ downloadLocks.set(lockKey, {
243
+ promise: downloadPromise,
244
+ timestamp: Date.now()
245
+ });
246
+
247
+ try {
248
+ const result = await downloadPromise;
249
+ return result;
250
+ } finally {
251
+ // Clean up lock after download completes (success or failure)
252
+ downloadLocks.delete(lockKey);
253
+ }
254
+ }
255
+
32
256
  /**
33
257
  * Detects the current OS and architecture
34
258
  * @returns {Object} Object containing OS and architecture information
@@ -644,7 +868,86 @@ async function getPackageVersion() {
644
868
  }
645
869
 
646
870
  /**
647
- * Downloads the probe binary
871
+ * Internal function that performs the actual download
872
+ * @param {string} version - Version to download
873
+ * @returns {Promise<string>} Path to the downloaded binary
874
+ */
875
+ async function doDownload(version) {
876
+ // Get writable directory for binary storage (handles CI, npx, Docker scenarios)
877
+ const localDir = await getPackageBinDir();
878
+
879
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
880
+ console.log(`Downloading probe binary (version: ${version || 'latest'})...`);
881
+ console.log(`Using binary directory: ${localDir}`);
882
+ }
883
+
884
+ const isWindows = os.platform() === 'win32';
885
+ // Use the correct binary name: probe.exe for Windows, probe-binary for Unix
886
+ const binaryName = isWindows ? `${BINARY_NAME}.exe` : `${BINARY_NAME}-binary`;
887
+ const binaryPath = path.join(localDir, binaryName);
888
+
889
+ // Get OS and architecture information
890
+ const { os: osInfo, arch: archInfo } = detectOsArch();
891
+
892
+ // Determine which version to download
893
+ let versionToUse = version;
894
+ let bestAsset;
895
+ let tagVersion;
896
+
897
+ if (!versionToUse || versionToUse === '0.0.0') {
898
+ // No specific version - use GitHub API to get the latest release
899
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
900
+ console.log('No specific version requested, will use the latest release');
901
+ }
902
+ const { tag, assets } = await getLatestRelease(undefined);
903
+ tagVersion = tag.startsWith('v') ? tag.substring(1) : tag;
904
+ bestAsset = findBestAsset(assets, osInfo, archInfo);
905
+
906
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
907
+ console.log(`Found release version: ${tagVersion}`);
908
+ }
909
+ } else {
910
+ // Specific version requested - construct download URL directly
911
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
912
+ console.log(`Direct download for version: ${versionToUse}`);
913
+ }
914
+ tagVersion = versionToUse;
915
+ bestAsset = constructAssetInfo(versionToUse, osInfo, archInfo);
916
+ }
917
+ const { assetPath, checksumPath } = await downloadAsset(bestAsset, localDir);
918
+
919
+ // Verify checksum if available
920
+ const checksumValid = await verifyChecksum(assetPath, checksumPath);
921
+ if (!checksumValid) {
922
+ throw new Error('Checksum verification failed');
923
+ }
924
+
925
+ // Extract the binary
926
+ const extractedBinaryPath = await extractBinary(assetPath, localDir);
927
+
928
+ // Save the version information
929
+ await saveVersionInfo(tagVersion, localDir);
930
+
931
+ // Clean up the downloaded archive
932
+ try {
933
+ await fs.remove(assetPath);
934
+ if (checksumPath) {
935
+ await fs.remove(checksumPath);
936
+ }
937
+ } catch (err) {
938
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
939
+ console.log(`Warning: Could not clean up temporary files: ${err}`);
940
+ }
941
+ }
942
+
943
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
944
+ console.log(`Binary successfully installed at ${extractedBinaryPath} (version: ${tagVersion})`);
945
+ }
946
+ return extractedBinaryPath;
947
+ }
948
+
949
+ /**
950
+ * Downloads the probe binary with download locking to prevent concurrent downloads
648
951
  * @param {string} [version] - Specific version to download
649
952
  * @returns {Promise<string>} Path to the downloaded binary
650
953
  */
@@ -658,13 +961,7 @@ export async function downloadProbeBinary(version) {
658
961
  version = await getPackageVersion();
659
962
  }
660
963
 
661
- if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
662
- console.log(`Downloading probe binary (version: ${version || 'latest'})...`);
663
- console.log(`Using binary directory: ${localDir}`);
664
- }
665
-
666
964
  const isWindows = os.platform() === 'win32';
667
- // Use the correct binary name: probe.exe for Windows, probe-binary for Unix
668
965
  const binaryName = isWindows ? `${BINARY_NAME}.exe` : `${BINARY_NAME}-binary`;
669
966
  const binaryPath = path.join(localDir, binaryName);
670
967
 
@@ -685,64 +982,55 @@ export async function downloadProbeBinary(version) {
685
982
  }
686
983
  }
687
984
 
688
- // Get OS and architecture information
689
- const { os: osInfo, arch: archInfo } = detectOsArch();
690
-
691
- // Determine which version to download
692
- let versionToUse = version;
693
- let bestAsset;
694
- let tagVersion;
695
-
696
- if (!versionToUse || versionToUse === '0.0.0') {
697
- // No specific version - use GitHub API to get the latest release
698
- if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
699
- console.log('No specific version requested, will use the latest release');
700
- }
701
- const { tag, assets } = await getLatestRelease(undefined);
702
- tagVersion = tag.startsWith('v') ? tag.substring(1) : tag;
703
- bestAsset = findBestAsset(assets, osInfo, archInfo);
704
-
705
- if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
706
- console.log(`Found release version: ${tagVersion}`);
707
- }
708
- } else {
709
- // Specific version requested - construct download URL directly
710
- if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
711
- console.log(`Direct download for version: ${versionToUse}`);
985
+ // File-based lock for cross-process coordination
986
+ const lockPath = path.join(localDir, `.probe-download-${version}.lock`);
987
+
988
+ // Try to acquire file lock with retries
989
+ const maxRetries = 3;
990
+ for (let retry = 0; retry < maxRetries; retry++) {
991
+ const lockAcquired = await acquireFileLock(lockPath, version);
992
+
993
+ if (lockAcquired === true) {
994
+ // We got the lock - do the download
995
+ try {
996
+ const result = await withDownloadLock(version, () => doDownload(version));
997
+ return result;
998
+ } finally {
999
+ // Always release file lock
1000
+ await releaseFileLock(lockPath);
1001
+ }
712
1002
  }
713
- tagVersion = versionToUse;
714
- bestAsset = constructAssetInfo(versionToUse, osInfo, archInfo);
715
- }
716
- const { assetPath, checksumPath } = await downloadAsset(bestAsset, localDir);
717
-
718
- // Verify checksum if available
719
- const checksumValid = await verifyChecksum(assetPath, checksumPath);
720
- if (!checksumValid) {
721
- throw new Error('Checksum verification failed');
722
- }
723
1003
 
724
- // Extract the binary
725
- const extractedBinaryPath = await extractBinary(assetPath, localDir);
1004
+ if (lockAcquired === null) {
1005
+ // File locking unavailable (permissions/errors) - proceed without it
1006
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
1007
+ console.log(`File-based locking unavailable, downloading without cross-process coordination`);
1008
+ }
1009
+ return await withDownloadLock(version, () => doDownload(version));
1010
+ }
726
1011
 
727
- // Save the version information
728
- await saveVersionInfo(tagVersion, localDir);
1012
+ // lockAcquired === false: Lock not acquired - another process is downloading
1013
+ // Wait for the download to complete
1014
+ const downloadCompleted = await waitForFileLock(lockPath, binaryPath);
729
1015
 
730
- // Clean up the downloaded archive
731
- try {
732
- await fs.remove(assetPath);
733
- if (checksumPath) {
734
- await fs.remove(checksumPath);
1016
+ if (downloadCompleted) {
1017
+ // Binary is now available
1018
+ return binaryPath;
735
1019
  }
736
- } catch (err) {
737
- if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
738
- console.log(`Warning: Could not clean up temporary files: ${err}`);
1020
+
1021
+ // Download failed or lock became stale - retry
1022
+ if (retry < maxRetries - 1) {
1023
+ if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
1024
+ console.log(`Retrying download (attempt ${retry + 2}/${maxRetries})...`);
1025
+ }
739
1026
  }
740
1027
  }
741
1028
 
1029
+ // All retries exhausted - try one last download without lock
742
1030
  if (process.env.DEBUG === '1' || process.env.VERBOSE === '1') {
743
- console.log(`Binary successfully installed at ${extractedBinaryPath} (version: ${tagVersion})`);
1031
+ console.log(`All lock attempts exhausted, attempting direct download`);
744
1032
  }
745
- return extractedBinaryPath;
1033
+ return await withDownloadLock(version, () => doDownload(version));
746
1034
  } catch (error) {
747
1035
  console.error('Error downloading probe binary:', error);
748
1036
  throw error;
@@ -128,12 +128,17 @@ class ProbeServer {
128
128
  },
129
129
  query: {
130
130
  type: 'string',
131
- description: 'ElasticSearch query syntax. MUST use explicit AND/OR operators and parentheses for grouping. For exact matches, ALWAYS wrap terms in quotes. Examples: "functionName" (exact match), (error AND handler), ("getUserId" AND NOT deprecated)',
131
+ description: 'ElasticSearch query syntax. Use explicit AND/OR operators and parentheses for grouping. For exact matches, wrap terms in quotes. Examples: "functionName" (exact match), (error AND handler), ("getUserId" AND NOT deprecated)',
132
132
  },
133
133
  exact: {
134
134
  type: 'boolean',
135
135
  description: 'Use when searching for exact function/class/variable names',
136
136
  default: false
137
+ },
138
+ strictElasticSyntax: {
139
+ type: 'boolean',
140
+ description: 'Enforce strict ElasticSearch query syntax (require explicit AND/OR operators and quotes for exact matches)',
141
+ default: false
137
142
  }
138
143
  },
139
144
  required: ['path', 'query']
@@ -271,11 +276,13 @@ class ProbeServer {
271
276
  session: "new", // Fresh session each time
272
277
  maxResults: 20, // Reasonable limit for context window
273
278
  maxTokens: 8000, // Fits in most AI context windows
274
- strictElasticSyntax: true, // Enforce strict ES syntax in MCP mode
279
+ strictElasticSyntax: false, // Relaxed syntax by default in MCP mode
275
280
  };
276
281
  // Only override defaults if user explicitly set them
277
282
  if (args.exact !== undefined)
278
283
  options.exact = args.exact;
284
+ if (args.strictElasticSyntax !== undefined)
285
+ options.strictElasticSyntax = args.strictElasticSyntax;
279
286
  // Handle format based on server default
280
287
  if (this.defaultFormat === 'outline' || this.defaultFormat === 'outline-xml') {
281
288
  options.format = this.defaultFormat;
@@ -113,6 +113,7 @@ interface SearchCodeArgs {
113
113
  path: string;
114
114
  query: string | string[];
115
115
  exact?: boolean;
116
+ strictElasticSyntax?: boolean;
116
117
  }
117
118
 
118
119
  interface ExtractCodeArgs {
@@ -175,12 +176,17 @@ class ProbeServer {
175
176
  },
176
177
  query: {
177
178
  type: 'string',
178
- description: 'ElasticSearch query syntax. MUST use explicit AND/OR operators and parentheses for grouping. For exact matches, ALWAYS wrap terms in quotes. Examples: "functionName" (exact match), (error AND handler), ("getUserId" AND NOT deprecated)',
179
+ description: 'ElasticSearch query syntax. Use explicit AND/OR operators and parentheses for grouping. For exact matches, wrap terms in quotes. Examples: "functionName" (exact match), (error AND handler), ("getUserId" AND NOT deprecated)',
179
180
  },
180
181
  exact: {
181
182
  type: 'boolean',
182
183
  description: 'Use when searching for exact function/class/variable names',
183
184
  default: false
185
+ },
186
+ strictElasticSyntax: {
187
+ type: 'boolean',
188
+ description: 'Enforce strict ElasticSearch query syntax (require explicit AND/OR operators and quotes for exact matches)',
189
+ default: false
184
190
  }
185
191
  },
186
192
  required: ['path', 'query']
@@ -329,11 +335,12 @@ class ProbeServer {
329
335
  session: "new", // Fresh session each time
330
336
  maxResults: 20, // Reasonable limit for context window
331
337
  maxTokens: 8000, // Fits in most AI context windows
332
- strictElasticSyntax: true, // Enforce strict ES syntax in MCP mode
338
+ strictElasticSyntax: false, // Relaxed syntax by default in MCP mode
333
339
  };
334
340
 
335
341
  // Only override defaults if user explicitly set them
336
342
  if (args.exact !== undefined) options.exact = args.exact;
343
+ if (args.strictElasticSyntax !== undefined) options.strictElasticSyntax = args.strictElasticSyntax;
337
344
 
338
345
  // Handle format based on server default
339
346
  if (this.defaultFormat === 'outline' || this.defaultFormat === 'outline-xml') {
package/build/utils.js CHANGED
@@ -27,21 +27,23 @@ let probeBinaryPath = '';
27
27
  export async function getBinaryPath(options = {}) {
28
28
  const { forceDownload = false, version } = options;
29
29
 
30
- // Return cached path if available and not forcing download
31
- if (probeBinaryPath && !forceDownload && fs.existsSync(probeBinaryPath)) {
30
+ // Check environment variable first (user override)
31
+ if (process.env.PROBE_PATH && fs.existsSync(process.env.PROBE_PATH) && !forceDownload) {
32
+ probeBinaryPath = process.env.PROBE_PATH;
32
33
  return probeBinaryPath;
33
34
  }
34
35
 
35
- // Check environment variable
36
- if (process.env.PROBE_PATH && fs.existsSync(process.env.PROBE_PATH) && !forceDownload) {
37
- probeBinaryPath = process.env.PROBE_PATH;
36
+ // If specific version is requested, download it (don't use cached/postinstall binary)
37
+ if (version && !forceDownload) {
38
+ console.log(`Specific version ${version} requested. Downloading...`);
39
+ probeBinaryPath = await downloadProbeBinary(version);
38
40
  return probeBinaryPath;
39
41
  }
40
42
 
41
43
  // Get dynamic bin directory (handles CI, npx, Docker scenarios)
42
44
  const binDir = await getPackageBinDir();
43
-
44
- // Check bin directory
45
+
46
+ // Check postinstall binary in package directory (most up-to-date with npm package version)
45
47
  const isWindows = process.platform === 'win32';
46
48
  const binaryName = isWindows ? 'probe.exe' : 'probe-binary';
47
49
  const binaryPath = path.join(binDir, binaryName);