@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.
- package/README.md +528 -0
- package/bin/.gitkeep +0 -0
- package/bin/probe +0 -0
- package/build/mcp/index.js +499 -0
- package/build/mcp/index.ts +593 -0
- package/package.json +62 -0
- package/scripts/postinstall.js +127 -0
- package/src/cli.js +49 -0
- package/src/downloader.js +571 -0
- package/src/extract.js +147 -0
- package/src/index.js +55 -0
- package/src/mcp/index.ts +593 -0
- package/src/query.js +112 -0
- package/src/search.js +235 -0
- package/src/tools/common.js +224 -0
- package/src/tools/index.js +36 -0
- package/src/tools/langchain.js +88 -0
- package/src/tools/system-message.js +69 -0
- package/src/tools/vercel.js +215 -0
- package/src/utils/file-lister.js +193 -0
- package/src/utils.js +120 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary downloader for the probe package
|
|
3
|
+
* @module downloader
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import { exec as execCallback } from 'child_process';
|
|
12
|
+
import tar from 'tar';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { ensureBinDirectory } from './utils.js';
|
|
16
|
+
|
|
17
|
+
const exec = promisify(execCallback);
|
|
18
|
+
|
|
19
|
+
// GitHub repository information
|
|
20
|
+
const REPO_OWNER = "buger";
|
|
21
|
+
const REPO_NAME = "probe";
|
|
22
|
+
const BINARY_NAME = "probe";
|
|
23
|
+
|
|
24
|
+
// Get the directory of the current module
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
|
|
28
|
+
// Local storage directory for downloaded binaries
|
|
29
|
+
const LOCAL_DIR = path.resolve(__dirname, '..', 'bin');
|
|
30
|
+
|
|
31
|
+
// Version info file path
|
|
32
|
+
const VERSION_INFO_PATH = path.join(LOCAL_DIR, 'version-info.json');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detects the current OS and architecture
|
|
36
|
+
* @returns {Object} Object containing OS and architecture information
|
|
37
|
+
*/
|
|
38
|
+
function detectOsArch() {
|
|
39
|
+
const osType = os.platform();
|
|
40
|
+
const archType = os.arch();
|
|
41
|
+
|
|
42
|
+
let osInfo;
|
|
43
|
+
let archInfo;
|
|
44
|
+
|
|
45
|
+
// Detect OS
|
|
46
|
+
switch (osType) {
|
|
47
|
+
case 'linux':
|
|
48
|
+
osInfo = {
|
|
49
|
+
type: 'linux',
|
|
50
|
+
keywords: ['linux', 'Linux', 'gnu']
|
|
51
|
+
};
|
|
52
|
+
break;
|
|
53
|
+
case 'darwin':
|
|
54
|
+
osInfo = {
|
|
55
|
+
type: 'darwin',
|
|
56
|
+
keywords: ['darwin', 'Darwin', 'mac', 'Mac', 'apple', 'Apple', 'osx', 'OSX']
|
|
57
|
+
};
|
|
58
|
+
break;
|
|
59
|
+
case 'win32':
|
|
60
|
+
osInfo = {
|
|
61
|
+
type: 'windows',
|
|
62
|
+
keywords: ['windows', 'Windows', 'msvc', 'pc-windows']
|
|
63
|
+
};
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`Unsupported operating system: ${osType}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Detect architecture
|
|
70
|
+
switch (archType) {
|
|
71
|
+
case 'x64':
|
|
72
|
+
archInfo = {
|
|
73
|
+
type: 'x86_64',
|
|
74
|
+
keywords: ['x86_64', 'amd64', 'x64', '64bit', '64-bit']
|
|
75
|
+
};
|
|
76
|
+
break;
|
|
77
|
+
case 'arm64':
|
|
78
|
+
archInfo = {
|
|
79
|
+
type: 'aarch64',
|
|
80
|
+
keywords: ['arm64', 'aarch64', 'arm', 'ARM']
|
|
81
|
+
};
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`Unsupported architecture: ${archType}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`Detected OS: ${osInfo.type}, Architecture: ${archInfo.type}`);
|
|
88
|
+
return { os: osInfo, arch: archInfo };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Gets the latest release information from GitHub
|
|
93
|
+
* @param {string} [version] - Specific version to get
|
|
94
|
+
* @returns {Promise<Object>} Release information
|
|
95
|
+
*/
|
|
96
|
+
async function getLatestRelease(version) {
|
|
97
|
+
console.log('Fetching release information...');
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
let releaseUrl;
|
|
101
|
+
if (version) {
|
|
102
|
+
// Always use the specified version
|
|
103
|
+
releaseUrl = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/v${version}`;
|
|
104
|
+
} else {
|
|
105
|
+
// Use the latest release only if no version is specified
|
|
106
|
+
releaseUrl = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const response = await axios.get(releaseUrl);
|
|
110
|
+
|
|
111
|
+
if (response.status !== 200) {
|
|
112
|
+
throw new Error(`Failed to fetch release information: ${response.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tag = response.data.tag_name;
|
|
116
|
+
const assets = response.data.assets.map(asset => ({
|
|
117
|
+
name: asset.name,
|
|
118
|
+
url: asset.browser_download_url
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
console.log(`Found release: ${tag} with ${assets.length} assets`);
|
|
122
|
+
return { tag, assets };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
125
|
+
// If the specific version is not found, try to get all releases
|
|
126
|
+
console.log(`Release v${version} not found, trying to fetch all releases...`);
|
|
127
|
+
|
|
128
|
+
const response = await axios.get(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases`);
|
|
129
|
+
|
|
130
|
+
if (response.data.length === 0) {
|
|
131
|
+
throw new Error('No releases found');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Try to find a release that matches the version prefix
|
|
135
|
+
let bestRelease = response.data[0]; // Default to first release
|
|
136
|
+
|
|
137
|
+
if (version && version !== '0.0.0') {
|
|
138
|
+
// Try to find a release that starts with the same version prefix
|
|
139
|
+
const versionParts = version.split('.');
|
|
140
|
+
const versionPrefix = versionParts.slice(0, 2).join('.'); // e.g., "0.2" from "0.2.2-rc7"
|
|
141
|
+
|
|
142
|
+
console.log(`Looking for releases matching prefix: ${versionPrefix}`);
|
|
143
|
+
|
|
144
|
+
for (const release of response.data) {
|
|
145
|
+
const releaseTag = release.tag_name.startsWith('v') ?
|
|
146
|
+
release.tag_name.substring(1) : release.tag_name;
|
|
147
|
+
|
|
148
|
+
if (releaseTag.startsWith(versionPrefix)) {
|
|
149
|
+
console.log(`Found matching release: ${release.tag_name}`);
|
|
150
|
+
bestRelease = release;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const tag = bestRelease.tag_name;
|
|
157
|
+
const assets = bestRelease.assets.map(asset => ({
|
|
158
|
+
name: asset.name,
|
|
159
|
+
url: asset.browser_download_url
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
console.log(`Using release: ${tag} with ${assets.length} assets`);
|
|
163
|
+
return { tag, assets };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Finds the best matching asset for the current OS and architecture
|
|
172
|
+
* @param {Array} assets - List of assets
|
|
173
|
+
* @param {Object} osInfo - OS information
|
|
174
|
+
* @param {Object} archInfo - Architecture information
|
|
175
|
+
* @returns {Object} Best matching asset
|
|
176
|
+
*/
|
|
177
|
+
function findBestAsset(assets, osInfo, archInfo) {
|
|
178
|
+
console.log(`Finding appropriate binary for ${osInfo.type} ${archInfo.type}...`);
|
|
179
|
+
|
|
180
|
+
let bestAsset = null;
|
|
181
|
+
let bestScore = 0;
|
|
182
|
+
|
|
183
|
+
for (const asset of assets) {
|
|
184
|
+
// Skip checksum files
|
|
185
|
+
if (asset.name.endsWith('.sha256') || asset.name.endsWith('.md5') || asset.name.endsWith('.asc')) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (osInfo.type === 'windows' && asset.name.match(/darwin|linux/)) {
|
|
190
|
+
console.log(`Skipping non-Windows binary: ${asset.name}`);
|
|
191
|
+
continue;
|
|
192
|
+
} else if (osInfo.type === 'darwin' && asset.name.match(/windows|msvc|linux/)) {
|
|
193
|
+
console.log(`Skipping non-macOS binary: ${asset.name}`);
|
|
194
|
+
continue;
|
|
195
|
+
} else if (osInfo.type === 'linux' && asset.name.match(/darwin|windows|msvc/)) {
|
|
196
|
+
console.log(`Skipping non-Linux binary: ${asset.name}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let score = 0;
|
|
201
|
+
console.log(`Evaluating asset: ${asset.name}`);
|
|
202
|
+
|
|
203
|
+
// Check for OS match - give higher priority to exact OS matches
|
|
204
|
+
let osMatched = false;
|
|
205
|
+
for (const keyword of osInfo.keywords) {
|
|
206
|
+
if (asset.name.includes(keyword)) {
|
|
207
|
+
score += 10;
|
|
208
|
+
osMatched = true;
|
|
209
|
+
console.log(` OS match found (${keyword}): +10, score = ${score}`);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check for architecture match
|
|
215
|
+
for (const keyword of archInfo.keywords) {
|
|
216
|
+
if (asset.name.includes(keyword)) {
|
|
217
|
+
score += 5;
|
|
218
|
+
console.log(` Arch match found (${keyword}): +5, score = ${score}`);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Prefer exact matches for binary name
|
|
224
|
+
if (asset.name.startsWith(`${BINARY_NAME}-`)) {
|
|
225
|
+
score += 3;
|
|
226
|
+
console.log(` Binary name match: +3, score = ${score}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (osMatched && score >= 15) {
|
|
230
|
+
score += 5;
|
|
231
|
+
console.log(` OS+Arch bonus: +5, score = ${score}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(` Final score for ${asset.name}: ${score}`);
|
|
235
|
+
|
|
236
|
+
// If we have a perfect match, use it immediately
|
|
237
|
+
if (score === 23) {
|
|
238
|
+
console.log(`Found perfect match: ${asset.name}`);
|
|
239
|
+
return asset;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Otherwise, keep track of the best match so far
|
|
243
|
+
if (score > bestScore) {
|
|
244
|
+
bestScore = score;
|
|
245
|
+
bestAsset = asset;
|
|
246
|
+
console.log(` New best asset: ${asset.name} (score: ${score})`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!bestAsset) {
|
|
251
|
+
throw new Error(`Could not find a suitable binary for ${osInfo.type} ${archInfo.type}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`Selected asset: ${bestAsset.name} (score: ${bestScore})`);
|
|
255
|
+
return bestAsset;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Downloads the asset and its checksum
|
|
260
|
+
* @param {Object} asset - Asset to download
|
|
261
|
+
* @param {string} outputDir - Directory to save to
|
|
262
|
+
* @returns {Promise<Object>} Paths to the asset and checksum
|
|
263
|
+
*/
|
|
264
|
+
async function downloadAsset(asset, outputDir) {
|
|
265
|
+
await fs.ensureDir(outputDir);
|
|
266
|
+
|
|
267
|
+
const assetPath = path.join(outputDir, asset.name);
|
|
268
|
+
console.log(`Downloading ${asset.name}...`);
|
|
269
|
+
|
|
270
|
+
// Download the asset
|
|
271
|
+
const assetResponse = await axios.get(asset.url, { responseType: 'arraybuffer' });
|
|
272
|
+
await fs.writeFile(assetPath, Buffer.from(assetResponse.data));
|
|
273
|
+
|
|
274
|
+
// Try to download the checksum
|
|
275
|
+
const checksumUrl = `${asset.url}.sha256`;
|
|
276
|
+
let checksumPath = null;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
console.log(`Downloading checksum...`);
|
|
280
|
+
const checksumResponse = await axios.get(checksumUrl);
|
|
281
|
+
checksumPath = path.join(outputDir, `${asset.name}.sha256`);
|
|
282
|
+
await fs.writeFile(checksumPath, checksumResponse.data);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.log('No checksum file found, skipping verification');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { assetPath, checksumPath };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Verifies the checksum of the downloaded asset
|
|
292
|
+
* @param {string} assetPath - Path to the asset
|
|
293
|
+
* @param {string|null} checksumPath - Path to the checksum file
|
|
294
|
+
* @returns {Promise<boolean>} Whether verification succeeded
|
|
295
|
+
*/
|
|
296
|
+
async function verifyChecksum(assetPath, checksumPath) {
|
|
297
|
+
if (!checksumPath) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(`Verifying checksum...`);
|
|
302
|
+
|
|
303
|
+
// Read the expected checksum
|
|
304
|
+
const checksumContent = await fs.readFile(checksumPath, 'utf-8');
|
|
305
|
+
const expectedChecksum = checksumContent.trim().split(' ')[0];
|
|
306
|
+
|
|
307
|
+
// Calculate the actual checksum
|
|
308
|
+
const fileBuffer = await fs.readFile(assetPath);
|
|
309
|
+
const actualChecksum = createHash('sha256').update(fileBuffer).digest('hex');
|
|
310
|
+
|
|
311
|
+
if (expectedChecksum !== actualChecksum) {
|
|
312
|
+
console.error(`Checksum verification failed!`);
|
|
313
|
+
console.error(`Expected: ${expectedChecksum}`);
|
|
314
|
+
console.error(`Actual: ${actualChecksum}`);
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log(`Checksum verified successfully`);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extracts and installs the binary
|
|
324
|
+
* @param {string} assetPath - Path to the asset
|
|
325
|
+
* @param {string} outputDir - Directory to extract to
|
|
326
|
+
* @returns {Promise<string>} Path to the extracted binary
|
|
327
|
+
*/
|
|
328
|
+
async function extractBinary(assetPath, outputDir) {
|
|
329
|
+
console.log(`Extracting ${path.basename(assetPath)}...`);
|
|
330
|
+
|
|
331
|
+
const assetName = path.basename(assetPath);
|
|
332
|
+
const isWindows = os.platform() === 'win32';
|
|
333
|
+
const binaryName = isWindows ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
334
|
+
const binaryPath = path.join(outputDir, binaryName);
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
// Create a temporary extraction directory
|
|
338
|
+
const extractDir = path.join(outputDir, 'temp_extract');
|
|
339
|
+
await fs.ensureDir(extractDir);
|
|
340
|
+
|
|
341
|
+
// Determine file type and extract accordingly
|
|
342
|
+
if (assetName.endsWith('.tar.gz') || assetName.endsWith('.tgz')) {
|
|
343
|
+
console.log(`Extracting tar.gz to ${extractDir}...`);
|
|
344
|
+
await tar.extract({
|
|
345
|
+
file: assetPath,
|
|
346
|
+
cwd: extractDir
|
|
347
|
+
});
|
|
348
|
+
} else if (assetName.endsWith('.zip')) {
|
|
349
|
+
console.log(`Extracting zip to ${extractDir}...`);
|
|
350
|
+
await exec(`unzip -q "${assetPath}" -d "${extractDir}"`);
|
|
351
|
+
} else {
|
|
352
|
+
// Assume it's a direct binary
|
|
353
|
+
console.log(`Copying binary directly to ${binaryPath}`);
|
|
354
|
+
await fs.copyFile(assetPath, binaryPath);
|
|
355
|
+
|
|
356
|
+
// Make the binary executable
|
|
357
|
+
if (!isWindows) {
|
|
358
|
+
await fs.chmod(binaryPath, 0o755);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Clean up the extraction directory
|
|
362
|
+
await fs.remove(extractDir);
|
|
363
|
+
console.log(`Binary installed to ${binaryPath}`);
|
|
364
|
+
return binaryPath;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Find the binary in the extracted files
|
|
368
|
+
console.log(`Searching for binary in extracted files...`);
|
|
369
|
+
const findBinary = async (dir) => {
|
|
370
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
371
|
+
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
const fullPath = path.join(dir, entry.name);
|
|
374
|
+
|
|
375
|
+
if (entry.isDirectory()) {
|
|
376
|
+
const result = await findBinary(fullPath);
|
|
377
|
+
if (result) return result;
|
|
378
|
+
} else if (entry.isFile()) {
|
|
379
|
+
// Check if this is the binary we're looking for
|
|
380
|
+
if (entry.name === binaryName ||
|
|
381
|
+
entry.name === BINARY_NAME ||
|
|
382
|
+
(isWindows && entry.name.endsWith('.exe'))) {
|
|
383
|
+
return fullPath;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return null;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const binaryFilePath = await findBinary(extractDir);
|
|
392
|
+
|
|
393
|
+
if (!binaryFilePath) {
|
|
394
|
+
// List all extracted files for debugging
|
|
395
|
+
const allFiles = await fs.readdir(extractDir, { recursive: true });
|
|
396
|
+
console.error(`Binary not found in extracted files. Found: ${allFiles.join(', ')}`);
|
|
397
|
+
throw new Error(`Binary not found in the archive.`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Copy the binary directly to the final location
|
|
401
|
+
console.log(`Found binary at ${binaryFilePath}`);
|
|
402
|
+
console.log(`Copying binary to ${binaryPath}`);
|
|
403
|
+
await fs.copyFile(binaryFilePath, binaryPath);
|
|
404
|
+
|
|
405
|
+
// Make the binary executable
|
|
406
|
+
if (!isWindows) {
|
|
407
|
+
await fs.chmod(binaryPath, 0o755);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Clean up
|
|
411
|
+
await fs.remove(extractDir);
|
|
412
|
+
|
|
413
|
+
console.log(`Binary successfully installed to ${binaryPath}`);
|
|
414
|
+
return binaryPath;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error(`Error extracting binary: ${error instanceof Error ? error.message : String(error)}`);
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Gets version info from the version file
|
|
423
|
+
* @returns {Promise<Object|null>} Version information
|
|
424
|
+
*/
|
|
425
|
+
async function getVersionInfo() {
|
|
426
|
+
try {
|
|
427
|
+
if (await fs.pathExists(VERSION_INFO_PATH)) {
|
|
428
|
+
const content = await fs.readFile(VERSION_INFO_PATH, 'utf-8');
|
|
429
|
+
return JSON.parse(content);
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.warn(`Warning: Could not read version info: ${error}`);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Saves version info to the version file
|
|
440
|
+
* @param {string} version - Version to save
|
|
441
|
+
* @returns {Promise<void>}
|
|
442
|
+
*/
|
|
443
|
+
async function saveVersionInfo(version) {
|
|
444
|
+
const versionInfo = {
|
|
445
|
+
version,
|
|
446
|
+
lastUpdated: new Date().toISOString()
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
await fs.writeFile(VERSION_INFO_PATH, JSON.stringify(versionInfo, null, 2));
|
|
450
|
+
console.log(`Version info saved: ${version}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Gets the package version from package.json
|
|
455
|
+
* @returns {Promise<string>} Package version
|
|
456
|
+
*/
|
|
457
|
+
async function getPackageVersion() {
|
|
458
|
+
try {
|
|
459
|
+
// Try multiple possible locations for package.json
|
|
460
|
+
const possiblePaths = [
|
|
461
|
+
path.resolve(__dirname, '..', 'package.json'), // When installed from npm: src/../package.json
|
|
462
|
+
path.resolve(__dirname, '..', '..', 'package.json') // In development: src/../../package.json
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
for (const packageJsonPath of possiblePaths) {
|
|
466
|
+
try {
|
|
467
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
468
|
+
console.log(`Found package.json at: ${packageJsonPath}`);
|
|
469
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
470
|
+
if (packageJson.version) {
|
|
471
|
+
console.log(`Using version from package.json: ${packageJson.version}`);
|
|
472
|
+
return packageJson.version;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch (err) {
|
|
476
|
+
console.error(`Error reading package.json at ${packageJsonPath}:`, err);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// If we can't find the version in package.json, return a default version
|
|
481
|
+
return '0.0.0';
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error('Error getting package version:', error);
|
|
484
|
+
return '0.0.0';
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Downloads the probe binary
|
|
490
|
+
* @param {string} [version] - Specific version to download
|
|
491
|
+
* @returns {Promise<string>} Path to the downloaded binary
|
|
492
|
+
*/
|
|
493
|
+
export async function downloadProbeBinary(version) {
|
|
494
|
+
try {
|
|
495
|
+
// Create the bin directory if it doesn't exist
|
|
496
|
+
await ensureBinDirectory();
|
|
497
|
+
|
|
498
|
+
// If no version is specified, use the package version
|
|
499
|
+
if (!version || version === '0.0.0') {
|
|
500
|
+
version = await getPackageVersion();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
console.log(`Downloading probe binary (version: ${version || 'latest'})...`);
|
|
504
|
+
|
|
505
|
+
const isWindows = os.platform() === 'win32';
|
|
506
|
+
const binaryName = isWindows ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
507
|
+
const binaryPath = path.join(LOCAL_DIR, binaryName);
|
|
508
|
+
|
|
509
|
+
// Check if the binary already exists and version matches
|
|
510
|
+
if (await fs.pathExists(binaryPath)) {
|
|
511
|
+
const versionInfo = await getVersionInfo();
|
|
512
|
+
|
|
513
|
+
// If versions match, use existing binary
|
|
514
|
+
if (versionInfo && versionInfo.version === version) {
|
|
515
|
+
console.log(`Using existing binary at ${binaryPath} (version: ${versionInfo.version})`);
|
|
516
|
+
return binaryPath;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
console.log(`Existing binary version (${versionInfo?.version || 'unknown'}) doesn't match requested version (${version}). Downloading new version...`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Get OS and architecture information
|
|
523
|
+
const { os: osInfo, arch: archInfo } = detectOsArch();
|
|
524
|
+
|
|
525
|
+
// Determine which version to download
|
|
526
|
+
let versionToUse = version;
|
|
527
|
+
if (!versionToUse || versionToUse === '0.0.0') {
|
|
528
|
+
console.log('No specific version requested, will use the latest release');
|
|
529
|
+
versionToUse = undefined;
|
|
530
|
+
} else {
|
|
531
|
+
console.log(`Looking for release with version: ${versionToUse}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Get release information
|
|
535
|
+
const { tag, assets } = await getLatestRelease(versionToUse);
|
|
536
|
+
const tagVersion = tag.startsWith('v') ? tag.substring(1) : tag;
|
|
537
|
+
console.log(`Found release version: ${tagVersion}`);
|
|
538
|
+
|
|
539
|
+
// Find and download the appropriate asset
|
|
540
|
+
const bestAsset = findBestAsset(assets, osInfo, archInfo);
|
|
541
|
+
const { assetPath, checksumPath } = await downloadAsset(bestAsset, LOCAL_DIR);
|
|
542
|
+
|
|
543
|
+
// Verify checksum if available
|
|
544
|
+
const checksumValid = await verifyChecksum(assetPath, checksumPath);
|
|
545
|
+
if (!checksumValid) {
|
|
546
|
+
throw new Error('Checksum verification failed');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Extract the binary
|
|
550
|
+
const extractedBinaryPath = await extractBinary(assetPath, LOCAL_DIR);
|
|
551
|
+
|
|
552
|
+
// Save the version information
|
|
553
|
+
await saveVersionInfo(tagVersion);
|
|
554
|
+
|
|
555
|
+
// Clean up the downloaded archive
|
|
556
|
+
try {
|
|
557
|
+
await fs.remove(assetPath);
|
|
558
|
+
if (checksumPath) {
|
|
559
|
+
await fs.remove(checksumPath);
|
|
560
|
+
}
|
|
561
|
+
} catch (err) {
|
|
562
|
+
console.log(`Warning: Could not clean up temporary files: ${err}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.log(`Binary successfully installed at ${extractedBinaryPath} (version: ${tagVersion})`);
|
|
566
|
+
return extractedBinaryPath;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
console.error('Error downloading probe binary:', error);
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
}
|
package/src/extract.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract functionality for the probe package
|
|
3
|
+
* @module extract
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { getBinaryPath, buildCliArgs, escapeString } from './utils.js';
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Flag mapping for extract options
|
|
14
|
+
* Maps option keys to command-line flags
|
|
15
|
+
*/
|
|
16
|
+
const EXTRACT_FLAG_MAP = {
|
|
17
|
+
allowTests: '--allow-tests',
|
|
18
|
+
contextLines: '--context',
|
|
19
|
+
format: '--format',
|
|
20
|
+
inputFile: '--input-file'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract code blocks from files
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} options - Extract options
|
|
27
|
+
* @param {string[]} [options.files] - Files to extract from (can include line numbers with colon, e.g., "/path/to/file.rs:10")
|
|
28
|
+
* @param {string} [options.inputFile] - Path to a file containing unstructured text to extract file paths from
|
|
29
|
+
* @param {boolean} [options.allowTests] - Include test files
|
|
30
|
+
* @param {number} [options.contextLines] - Number of context lines to include
|
|
31
|
+
* @param {string} [options.format] - Output format ('markdown', 'plain', 'json')
|
|
32
|
+
* @param {Object} [options.binaryOptions] - Options for getting the binary
|
|
33
|
+
* @param {boolean} [options.binaryOptions.forceDownload] - Force download even if binary exists
|
|
34
|
+
* @param {string} [options.binaryOptions.version] - Specific version to download
|
|
35
|
+
* @param {boolean} [options.json] - Return results as parsed JSON instead of string
|
|
36
|
+
* @returns {Promise<string|Object>} - Extracted code as string or parsed JSON
|
|
37
|
+
* @throws {Error} If the extraction fails
|
|
38
|
+
*/
|
|
39
|
+
export async function extract(options) {
|
|
40
|
+
if (!options) {
|
|
41
|
+
throw new Error('Options object is required');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Either files or inputFile must be provided
|
|
45
|
+
if ((!options.files || !Array.isArray(options.files) || options.files.length === 0) && !options.inputFile) {
|
|
46
|
+
throw new Error('Either files array or inputFile must be provided');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get the binary path
|
|
50
|
+
const binaryPath = await getBinaryPath(options.binaryOptions || {});
|
|
51
|
+
|
|
52
|
+
// Build CLI arguments from options
|
|
53
|
+
const cliArgs = buildCliArgs(options, EXTRACT_FLAG_MAP);
|
|
54
|
+
|
|
55
|
+
// If json option is true, override format to json
|
|
56
|
+
if (options.json && !options.format) {
|
|
57
|
+
cliArgs.push('--format', 'json');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add files as positional arguments if provided
|
|
61
|
+
if (options.files && Array.isArray(options.files) && options.files.length > 0) {
|
|
62
|
+
for (const file of options.files) {
|
|
63
|
+
cliArgs.push(escapeString(file));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create a single log record with all extract parameters
|
|
68
|
+
let logMessage = `\nExtract:`;
|
|
69
|
+
if (options.files && options.files.length > 0) {
|
|
70
|
+
logMessage += ` files="${options.files.join(', ')}"`;
|
|
71
|
+
}
|
|
72
|
+
if (options.inputFile) logMessage += ` inputFile="${options.inputFile}"`;
|
|
73
|
+
if (options.allowTests) logMessage += " allowTests=true";
|
|
74
|
+
if (options.contextLines) logMessage += ` contextLines=${options.contextLines}`;
|
|
75
|
+
if (options.format) logMessage += ` format=${options.format}`;
|
|
76
|
+
if (options.json) logMessage += " json=true";
|
|
77
|
+
console.error(logMessage);
|
|
78
|
+
|
|
79
|
+
// Execute command
|
|
80
|
+
const command = `${binaryPath} extract ${cliArgs.join(' ')}`;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const { stdout, stderr } = await execAsync(command);
|
|
84
|
+
|
|
85
|
+
if (stderr) {
|
|
86
|
+
console.error(`stderr: ${stderr}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse the output to extract token usage information
|
|
90
|
+
let tokenUsage = {
|
|
91
|
+
requestTokens: 0,
|
|
92
|
+
responseTokens: 0,
|
|
93
|
+
totalTokens: 0
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Calculate approximate request tokens
|
|
97
|
+
if (options.files && Array.isArray(options.files)) {
|
|
98
|
+
tokenUsage.requestTokens = options.files.join(' ').length / 4;
|
|
99
|
+
} else if (options.inputFile) {
|
|
100
|
+
tokenUsage.requestTokens = options.inputFile.length / 4;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Try to extract token information from the output
|
|
104
|
+
if (stdout.includes('Total tokens returned:')) {
|
|
105
|
+
const tokenMatch = stdout.match(/Total tokens returned: (\d+)/);
|
|
106
|
+
if (tokenMatch && tokenMatch[1]) {
|
|
107
|
+
tokenUsage.responseTokens = parseInt(tokenMatch[1], 10);
|
|
108
|
+
tokenUsage.totalTokens = tokenUsage.requestTokens + tokenUsage.responseTokens;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add token usage information to the output
|
|
113
|
+
let output = stdout;
|
|
114
|
+
|
|
115
|
+
// Add token usage information at the end if not already present
|
|
116
|
+
if (!output.includes('Token Usage:')) {
|
|
117
|
+
output += `\nToken Usage:\n Request tokens: ${tokenUsage.requestTokens}\n Response tokens: ${tokenUsage.responseTokens}\n Total tokens: ${tokenUsage.totalTokens}\n`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Parse JSON if requested or if format is json
|
|
121
|
+
if (options.json || options.format === 'json') {
|
|
122
|
+
try {
|
|
123
|
+
const jsonOutput = JSON.parse(stdout);
|
|
124
|
+
|
|
125
|
+
// Add token usage to JSON output
|
|
126
|
+
if (!jsonOutput.token_usage) {
|
|
127
|
+
jsonOutput.token_usage = {
|
|
128
|
+
request_tokens: tokenUsage.requestTokens,
|
|
129
|
+
response_tokens: tokenUsage.responseTokens,
|
|
130
|
+
total_tokens: tokenUsage.totalTokens
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return jsonOutput;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('Error parsing JSON output:', error);
|
|
137
|
+
return output; // Fall back to string output with token usage
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return output;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Enhance error message with command details
|
|
144
|
+
const errorMessage = `Error executing extract command: ${error.message}\nCommand: ${command}`;
|
|
145
|
+
throw new Error(errorMessage);
|
|
146
|
+
}
|
|
147
|
+
}
|