@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.
- package/build/agent/index.js +226 -43
- package/build/downloader.js +343 -55
- package/build/mcp/index.js +9 -2
- package/build/mcp/index.ts +9 -2
- package/build/utils.js +9 -7
- package/cjs/agent/ProbeAgent.cjs +226 -43
- package/cjs/index.cjs +226 -43
- package/package.json +1 -1
- package/src/downloader.js +343 -55
- package/src/mcp/index.ts +9 -2
- package/src/utils.js +9 -7
package/src/downloader.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
//
|
|
692
|
-
|
|
693
|
-
let
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if (checksumPath) {
|
|
734
|
-
await fs.remove(checksumPath);
|
|
1016
|
+
if (downloadCompleted) {
|
|
1017
|
+
// Binary is now available
|
|
1018
|
+
return binaryPath;
|
|
735
1019
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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(`
|
|
1031
|
+
console.log(`All lock attempts exhausted, attempting direct download`);
|
|
744
1032
|
}
|
|
745
|
-
return
|
|
1033
|
+
return await withDownloadLock(version, () => doDownload(version));
|
|
746
1034
|
} catch (error) {
|
|
747
1035
|
console.error('Error downloading probe binary:', error);
|
|
748
1036
|
throw error;
|
package/src/mcp/index.ts
CHANGED
|
@@ -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.
|
|
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:
|
|
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/src/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
|
-
//
|
|
31
|
-
if (
|
|
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
|
-
//
|
|
36
|
-
if (
|
|
37
|
-
|
|
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
|
|
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);
|