@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,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
+ }