avocavo 1.1.2 → 1.1.3

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.
Files changed (2) hide show
  1. package/bin/avocavo.js +111 -108
  2. package/package.json +1 -1
package/bin/avocavo.js CHANGED
@@ -18,37 +18,37 @@ function secureReadFile(filePath) {
18
18
  try {
19
19
  // Resolve the absolute path
20
20
  const resolvedPath = path.resolve(filePath);
21
-
21
+
22
22
  // Get the current working directory
23
23
  const cwd = process.cwd();
24
-
24
+
25
25
  // Check if the resolved path is within the current working directory or its subdirectories
26
26
  if (!resolvedPath.startsWith(cwd)) {
27
27
  throw new Error('File access denied: Path traversal detected. Only files within the current directory are allowed.');
28
28
  }
29
-
29
+
30
30
  // Additional security: block access to sensitive system files
31
31
  const basename = path.basename(resolvedPath).toLowerCase();
32
32
  const forbidden = ['passwd', 'shadow', 'hosts', '.env', '.git', 'config'];
33
33
  if (forbidden.some(name => basename.includes(name))) {
34
34
  throw new Error('File access denied: Access to system/configuration files is not allowed.');
35
35
  }
36
-
36
+
37
37
  // Check if file exists and is readable
38
38
  if (!fs.existsSync(resolvedPath)) {
39
39
  throw new Error(`File not found: ${filePath}`);
40
40
  }
41
-
41
+
42
42
  const stats = fs.statSync(resolvedPath);
43
43
  if (!stats.isFile()) {
44
44
  throw new Error(`Not a file: ${filePath}`);
45
45
  }
46
-
46
+
47
47
  // Limit file size to prevent memory issues (max 10MB)
48
48
  if (stats.size > 10 * 1024 * 1024) {
49
49
  throw new Error('File too large: Maximum file size is 10MB');
50
50
  }
51
-
51
+
52
52
  return fs.readFileSync(resolvedPath, 'utf8');
53
53
  } catch (error) {
54
54
  throw new Error(`Failed to read file: ${error.message}`);
@@ -77,23 +77,23 @@ program
77
77
  const success = await auth.login(options.provider, false); // Disable Supabase OAuth
78
78
  if (success) {
79
79
  console.log(chalk.green('āœ… Successfully logged in!'));
80
-
80
+
81
81
  // Check for API keys - always check server for all keys
82
82
  try {
83
83
  const KeyManager = require('../lib/keys');
84
84
  const keyManager = new KeyManager(auth, program.opts().baseUrl);
85
-
85
+
86
86
  // Get user's API keys from server
87
87
  const headers = await keyManager.getAuthHeaders();
88
88
  const response = await axios.get(`${program.opts().baseUrl}/api/keys`, { headers });
89
-
89
+
90
90
  if (response.data.keys && response.data.keys.length > 0) {
91
91
  const keyCount = response.data.keys.length;
92
-
92
+
93
93
  // Check if user already has a selected key
94
94
  let currentKey = await auth.getApiKey();
95
95
  let keyWorked = false;
96
-
96
+
97
97
  if (currentKey) {
98
98
  // Test the current selected key
99
99
  const api = new NutritionAPI(currentKey, program.opts().baseUrl, 30000, auth);
@@ -109,15 +109,15 @@ program
109
109
  }
110
110
  }
111
111
  }
112
-
112
+
113
113
  // If no working key and user has exactly 1 key, auto-select it
114
114
  if (!keyWorked && keyCount === 1) {
115
115
  console.log(chalk.cyan('šŸ”„ Auto-selecting your API key...'));
116
116
  const singleKey = response.data.keys[0];
117
-
117
+
118
118
  // Store the key as selected
119
119
  await auth.storeApiKeySecurely(auth.getUserInfo().email, singleKey.api_key);
120
-
120
+
121
121
  // Test the auto-selected key
122
122
  const api = new NutritionAPI(singleKey.api_key, program.opts().baseUrl, 30000, auth);
123
123
  try {
@@ -184,7 +184,7 @@ program
184
184
  .action(async () => {
185
185
  try {
186
186
  const isLoggedIn = auth.isLoggedIn();
187
-
187
+
188
188
  if (!isLoggedIn) {
189
189
  console.log(chalk.yellow('āš ļø Not logged in'));
190
190
  console.log(chalk.cyan('šŸ’” Run: avocavo login'));
@@ -194,12 +194,12 @@ program
194
194
  const userInfo = auth.getUserInfo();
195
195
  console.log(chalk.green('āœ… Logged in'));
196
196
  console.log(chalk.cyan(`šŸ“§ Email: ${userInfo.email || 'Unknown'}`));
197
-
197
+
198
198
  // Check for selected API key (separate from JWT auth token)
199
199
  const selectedApiKey = await auth.getApiKey();
200
200
  if (selectedApiKey && (selectedApiKey.startsWith('ak_') || selectedApiKey.startsWith('SK'))) {
201
201
  console.log(chalk.gray(`šŸ”‘ API Key: ${selectedApiKey.substring(0, 12)}...`));
202
-
202
+
203
203
  // Debug storage method (hidden from users)
204
204
  if (process.env.AVOCAVO_DEBUG) {
205
205
  const insecureStorage = auth.config.get('insecureStorage');
@@ -210,7 +210,7 @@ program
210
210
  }
211
211
  }
212
212
  }
213
-
213
+
214
214
  // Use selected API key for nutrition API calls, JWT for auth management
215
215
  if (selectedApiKey) {
216
216
  try {
@@ -219,7 +219,7 @@ program
219
219
  console.log(chalk.cyan(`šŸŽŸļø Tier: ${account.api_tier}`));
220
220
  console.log(chalk.cyan(`šŸ“ˆ Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`));
221
221
  console.log(chalk.cyan(`šŸ“… Reset: ${new Date(account.usage.reset_date).toLocaleDateString()}`));
222
-
222
+
223
223
  // Show detailed credit buckets if available
224
224
  if (account.credits && account.credits.total > 0) {
225
225
  console.log(chalk.magenta('\nšŸ’° Credit Buckets:'));
@@ -256,7 +256,7 @@ program
256
256
  return;
257
257
  }
258
258
  }
259
-
259
+
260
260
  console.log(chalk.cyan(`šŸ’” You have ${keysList.keys.length} API keys but none selected. Choose one to activate:`));
261
261
  console.log(chalk.cyan(' avocavo keys switch'));
262
262
  console.log(chalk.gray(' or'));
@@ -355,9 +355,9 @@ program
355
355
  const api = await getApiClient();
356
356
  const globalOpts = program.opts();
357
357
  const verbose = options.verbose || globalOpts.verbose;
358
-
358
+
359
359
  const result = await api.analyzeIngredient(ingredient, options.verify, verbose);
360
-
360
+
361
361
  if (program.opts().json) {
362
362
  console.log(JSON.stringify(result, null, 2));
363
363
  return;
@@ -366,55 +366,55 @@ program
366
366
  if (result.success) {
367
367
  console.log(chalk.green(`āœ… ${result.ingredient}`));
368
368
  console.log(formatNutrition(result.nutrition));
369
-
369
+
370
370
  // USDA information - show basic info by default, more with flags
371
371
  const usda = result.metadata?.usda_match;
372
372
  if (usda) {
373
373
  console.log(`\n šŸ“‹ ${chalk.gray('USDA Reference:')} ${chalk.cyan(usda.description)}`);
374
374
  console.log(` šŸ”¢ ${chalk.gray('FDC ID:')} ${chalk.yellow(usda.fdc_id)}`);
375
-
375
+
376
376
  // Always show USDA link for verification
377
- const verificationUrl = result.metadata?.usda_link ||
378
- `https://fdc.nal.usda.gov/fdc-app.html#/food-details/${usda.fdc_id}`;
377
+ const verificationUrl = result.metadata?.usda_link ||
378
+ `https://fdc.nal.usda.gov/fdc-app.html#/food-details/${usda.fdc_id}`;
379
379
  console.log(` šŸ”— ${chalk.gray('USDA Link:')} ${chalk.underline(verificationUrl)}`);
380
-
380
+
381
381
  // Additional details with flags
382
382
  if (verbose || options.debug) {
383
383
  console.log(` šŸ“Š ${chalk.gray('Data Type:')} ${chalk.blue(usda.data_type)}`);
384
384
  }
385
385
  }
386
-
386
+
387
387
  // Estimated grams and quality information
388
388
  const parsing = result.parsing;
389
389
  if (parsing?.estimated_grams) {
390
390
  console.log(`\n āš–ļø ${chalk.gray('Estimated Grams:')} ${chalk.magenta(parsing.estimated_grams + 'g')}`);
391
391
  }
392
-
392
+
393
393
  // Match quality (more reliable than confidence score)
394
394
  if (result.metadata?.match_quality) {
395
395
  console.log(` šŸŽÆ ${chalk.gray('Match Quality:')} ${chalk.blue(result.metadata.match_quality)}`);
396
396
  }
397
-
397
+
398
398
  // Only show confidence with verbose flag since it can be misleadingly low for good matches
399
399
  if ((verbose || options.debug) && result.metadata?.confidence !== undefined) {
400
400
  const confidence = (result.metadata.confidence * 100).toFixed(1);
401
401
  console.log(` šŸ“Š ${chalk.gray('Algorithm Confidence:')} ${chalk.yellow(confidence + '%')} ${chalk.gray('(internal scoring)')}`);
402
402
  }
403
-
403
+
404
404
  // Warning about null nutrients
405
405
  const nullNutrients = Object.entries(result.nutrition || {})
406
406
  .filter(([key, value]) => value === null)
407
407
  .map(([key]) => key);
408
-
408
+
409
409
  if (nullNutrients.length > 0) {
410
410
  console.log(`\n āš ļø ${chalk.yellow('Note:')} ${chalk.gray(`${nullNutrients.length} nutrients unavailable in USDA database`)}`);
411
411
  }
412
-
412
+
413
413
  // Performance metrics only with --verbose or --debug
414
414
  if (verbose || options.debug) {
415
415
  console.log(formatPerformanceMetrics(result));
416
416
  }
417
-
417
+
418
418
  // Quick performance indicators with specific flags
419
419
  if (options.timing) {
420
420
  const processingTime = result.metadata?.processing_time_ms;
@@ -422,8 +422,8 @@ program
422
422
  console.log(` ā±ļø ${chalk.gray('Processing Time:')} ${formatResponseTime(processingTime)}`);
423
423
  }
424
424
  }
425
-
426
-
425
+
426
+
427
427
  } else {
428
428
  console.log(chalk.red(`āŒ ${result.error}`));
429
429
  process.exit(1);
@@ -447,7 +447,7 @@ program
447
447
  .action(async (options) => {
448
448
  try {
449
449
  let ingredients = [];
450
-
450
+
451
451
  if (options.file) {
452
452
  const content = secureReadFile(options.file);
453
453
  ingredients = content.split('\n').map(line => line.trim()).filter(line => line);
@@ -476,11 +476,11 @@ program
476
476
  const api = await getApiClient();
477
477
  const globalOpts = program.opts();
478
478
  const verbose = options.verbose || globalOpts.verbose;
479
-
479
+
480
480
  console.log(chalk.cyan(`šŸ³ Analyzing recipe with ${ingredients.length} ingredients (${servings} servings)...`));
481
-
481
+
482
482
  const result = await api.analyzeRecipe(ingredients, servings, verbose);
483
-
483
+
484
484
  if (program.opts().json) {
485
485
  console.log(JSON.stringify(result, null, 2));
486
486
  return;
@@ -489,17 +489,17 @@ program
489
489
  if (result.success) {
490
490
  console.log(chalk.green('āœ… Recipe analysis complete!'));
491
491
  console.log('');
492
-
492
+
493
493
  // Total nutrition
494
494
  console.log(chalk.bold('šŸ“Š Total Nutrition:'));
495
495
  console.log(formatNutrition(result.nutrition.total));
496
496
  console.log('');
497
-
497
+
498
498
  // Per-serving nutrition
499
499
  console.log(chalk.bold(`šŸ½ļø Per Serving (${servings} servings):`));
500
500
  console.log(formatNutrition(result.nutrition.per_serving));
501
501
  console.log('');
502
-
502
+
503
503
  // Ingredient breakdown
504
504
  if (result.nutrition.ingredients && result.nutrition.ingredients.length > 0) {
505
505
  console.log(chalk.bold('šŸ“‹ Ingredient Breakdown:'));
@@ -513,24 +513,24 @@ program
513
513
  ing.nutrition ? `${ing.nutrition.fiber}g` : 'N/A',
514
514
  ing.nutrition ? `${ing.nutrition.sodium}mg` : 'N/A'
515
515
  ]);
516
-
516
+
517
517
  console.log(formatTable([
518
518
  ['Status', 'Ingredient', 'Calories', 'Protein', 'Fat', 'Carbs', 'Fiber', 'Sodium'],
519
519
  ...tableData
520
520
  ]));
521
-
521
+
522
522
  // USDA verification details - show based on flags
523
- const ingredientsWithUSDA = result.nutrition.ingredients.filter(ing =>
523
+ const ingredientsWithUSDA = result.nutrition.ingredients.filter(ing =>
524
524
  ing.metadata?.usda_match
525
525
  );
526
-
526
+
527
527
  if (ingredientsWithUSDA.length > 0) {
528
528
  console.log('');
529
-
529
+
530
530
  // Clean default: just show count
531
531
  const usdaCount = ingredientsWithUSDA.length;
532
532
  console.log(chalk.gray(`šŸŽÆ USDA verified: ${usdaCount}/${ingredients.length} ingredients`));
533
-
533
+
534
534
  // Detailed USDA info with --verbose or --verify
535
535
  if (options.verbose || options.verify) {
536
536
  console.log('');
@@ -539,12 +539,12 @@ program
539
539
  const usda = ing.metadata.usda_match;
540
540
  console.log(chalk.gray(` ${ing.ingredient} →`));
541
541
  console.log(chalk.cyan(` šŸ“‹ ${usda.description}`));
542
-
542
+
543
543
  if (options.verbose) {
544
544
  console.log(chalk.yellow(` šŸ”¢ FDC ID: ${usda.fdc_id}`));
545
545
  console.log(chalk.blue(` šŸ“Š Type: ${usda.data_type}`));
546
546
  }
547
-
547
+
548
548
  if (options.verify && usda.verification_url) {
549
549
  console.log(chalk.underline(` šŸ”— ${usda.verification_url}`));
550
550
  }
@@ -552,20 +552,20 @@ program
552
552
  }
553
553
  }
554
554
  }
555
-
555
+
556
556
  // Recipe-level performance metrics (NEW) - only with --verbose
557
557
  if (options.verbose || options.debug) {
558
558
  console.log('');
559
559
  console.log(chalk.gray('Recipe Performance:'));
560
-
560
+
561
561
  if (result.usda_matches !== undefined) {
562
562
  console.log(` šŸŽÆ ${chalk.gray('USDA Matches:')} ${chalk.green(result.usda_matches)}`);
563
563
  }
564
-
564
+
565
565
  if (result.processing_time_ms !== undefined) {
566
566
  console.log(` ā±ļø ${chalk.gray('Total Time:')} ${formatResponseTime(result.processing_time_ms)}`);
567
567
  }
568
-
568
+
569
569
  }
570
570
  } else {
571
571
  console.log(chalk.red(`āŒ ${result.error}`));
@@ -587,7 +587,7 @@ program
587
587
  .action(async (options) => {
588
588
  try {
589
589
  let ingredients = [];
590
-
590
+
591
591
  if (options.file) {
592
592
  const content = secureReadFile(options.file);
593
593
  ingredients = content.split('\n').map(line => line.trim()).filter(line => line);
@@ -606,11 +606,11 @@ program
606
606
  const api = await getApiClient();
607
607
  const globalOpts = program.opts();
608
608
  const verbose = options.verbose || globalOpts.verbose;
609
-
609
+
610
610
  console.log(chalk.cyan(`⚔ Batch analyzing ${ingredients.length} ingredients...`));
611
-
611
+
612
612
  const result = await api.analyzeBatch(ingredients, verbose);
613
-
613
+
614
614
  if (program.opts().json) {
615
615
  console.log(JSON.stringify(result, null, 2));
616
616
  return;
@@ -620,7 +620,7 @@ program
620
620
  console.log(chalk.green(`āœ… Batch analysis complete!`));
621
621
  console.log(chalk.cyan(`šŸ“Š Processed: ${result.summary.successful}/${result.batch_size} ingredients`));
622
622
  console.log('');
623
-
623
+
624
624
  // Enhanced results table with more nutrition info
625
625
  const tableData = result.results.map(item => [
626
626
  item.success ? 'āœ…' : 'āŒ',
@@ -632,12 +632,12 @@ program
632
632
  item.success ? `${item.nutrition.fiber}g` : 'N/A',
633
633
  item.success ? `${item.nutrition.sodium}mg` : 'N/A'
634
634
  ]);
635
-
635
+
636
636
  console.log(formatTable([
637
637
  ['Status', 'Ingredient', 'Calories', 'Protein', 'Fat', 'Carbs', 'Fiber', 'Sodium'],
638
638
  ...tableData
639
639
  ]));
640
-
640
+
641
641
  // Show USDA verification details for successful ingredients
642
642
  if (result.results.some(item => item.success && item.metadata?.usda_match)) {
643
643
  console.log('');
@@ -652,7 +652,7 @@ program
652
652
  }
653
653
  });
654
654
  }
655
-
655
+
656
656
  } else {
657
657
  console.log(chalk.red(`āŒ ${result.error}`));
658
658
  process.exit(1);
@@ -673,11 +673,11 @@ program
673
673
  const api = await getApiClient();
674
674
  const globalOpts = program.opts();
675
675
  const verbose = options.verbose || globalOpts.verbose;
676
-
676
+
677
677
  console.log(chalk.cyan(`šŸ” Searching UPC: ${upc}...`));
678
-
678
+
679
679
  const result = await api.searchUPC(upc);
680
-
680
+
681
681
  if (program.opts().json) {
682
682
  console.log(JSON.stringify(result, null, 2));
683
683
  return;
@@ -687,7 +687,7 @@ program
687
687
  const product = result.product;
688
688
  console.log(chalk.green(`āœ… Product Found!`));
689
689
  console.log('');
690
-
690
+
691
691
  // Basic product info
692
692
  if (product.product_name) {
693
693
  console.log(`šŸ“¦ ${chalk.bold('Product:')} ${chalk.cyan(product.product_name)}`);
@@ -698,19 +698,19 @@ program
698
698
  if (product.manufacturer) {
699
699
  console.log(`šŸ­ ${chalk.bold('Manufacturer:')} ${chalk.gray(product.manufacturer)}`);
700
700
  }
701
-
701
+
702
702
  // Data sources
703
703
  if (product.sources && product.sources.length > 0) {
704
704
  console.log(`šŸ“Š ${chalk.bold('Sources:')} ${chalk.magenta(product.sources.join(', '))}`);
705
705
  }
706
-
706
+
707
707
  // Categories
708
708
  if (product.categories && product.categories.length > 0) {
709
709
  const categoryDisplay = product.categories.slice(0, 3).join(', ');
710
710
  const moreCategories = product.categories.length > 3 ? ` (+${product.categories.length - 3} more)` : '';
711
711
  console.log(`šŸ“‹ ${chalk.bold('Categories:')} ${chalk.blue(categoryDisplay)}${chalk.gray(moreCategories)}`);
712
712
  }
713
-
713
+
714
714
  // Serving info
715
715
  if (product.serving_size) {
716
716
  console.log(`šŸ„„ ${chalk.bold('Serving Size:')} ${chalk.cyan(product.serving_size)}`);
@@ -718,58 +718,61 @@ program
718
718
  if (product.servings_per_container) {
719
719
  console.log(`šŸ“¦ ${chalk.bold('Servings Per Container:')} ${chalk.cyan(product.servings_per_container)}`);
720
720
  }
721
-
721
+
722
722
  // Nutrition data sample (if available)
723
723
  if (product.nutrition && typeof product.nutrition === 'object' && Object.keys(product.nutrition).length > 0) {
724
724
  console.log('');
725
725
  console.log(chalk.bold('šŸŽ Nutrition Data Available:'));
726
-
726
+
727
727
  // Show sample from merged nutrition if available
728
728
  const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition;
729
729
  if (nutritionData && typeof nutritionData === 'object') {
730
730
  const sampleNutrients = Object.entries(nutritionData)
731
731
  .slice(0, 5)
732
732
  .filter(([key, value]) => value !== null && value !== undefined);
733
-
733
+
734
734
  sampleNutrients.forEach(([nutrient, value]) => {
735
735
  console.log(` ${chalk.gray(nutrient.replace(/_/g, ' ').toUpperCase())}:`, chalk.green(value));
736
736
  });
737
-
737
+
738
738
  const totalNutrients = Object.keys(nutritionData).length;
739
739
  if (totalNutrients > 5) {
740
740
  console.log(` ${chalk.gray(`... and ${totalNutrients - 5} more nutrients`)}`);
741
741
  }
742
742
  }
743
743
  }
744
-
744
+
745
745
  // Verbose information
746
746
  if (verbose) {
747
747
  console.log('');
748
748
  console.log(chalk.bold('šŸ“ Additional Details:'));
749
-
749
+
750
750
  if (product.ingredients_text) {
751
- const ingredientsPreview = product.ingredients_text.length > 100 ?
751
+ const ingredientsPreview = product.ingredients_text.length > 100 ?
752
752
  product.ingredients_text.substring(0, 100) + '...' : product.ingredients_text;
753
753
  console.log(` ${chalk.bold('Ingredients:')} ${chalk.gray(ingredientsPreview)}`);
754
754
  }
755
-
755
+
756
756
  if (product.packaging) {
757
757
  console.log(` ${chalk.bold('Packaging:')} ${chalk.gray(product.packaging)}`);
758
758
  }
759
-
760
- if (product.countries && product.countries.length > 0) {
761
- console.log(` ${chalk.bold('Countries:')} ${chalk.gray(product.countries.join(', '))}`);
759
+
760
+ if (product.countries) {
761
+ const countriesStr = Array.isArray(product.countries) ? product.countries.join(', ') : product.countries;
762
+ if (countriesStr) {
763
+ console.log(` ${chalk.bold('Countries:')} ${chalk.gray(countriesStr)}`);
764
+ }
762
765
  }
763
-
766
+
764
767
  if (product.quality_score) {
765
768
  console.log(` ${chalk.bold('Quality Score:')} ${chalk.cyan(product.quality_score)}`);
766
769
  }
767
-
770
+
768
771
  if (product.images && product.images.length > 0) {
769
772
  console.log(` ${chalk.bold('Images:')} ${chalk.cyan(product.images.length)} available`);
770
773
  }
771
774
  }
772
-
775
+
773
776
  // Performance info
774
777
  if (result.processing_time_ms) {
775
778
  console.log('');
@@ -778,7 +781,7 @@ program
778
781
  if (result.from_cache) {
779
782
  console.log(`šŸ’¾ ${chalk.gray('Source:')} ${chalk.green('Cache (fast response)')}`);
780
783
  }
781
-
784
+
782
785
  } else {
783
786
  console.log(chalk.yellow(`āŒ Product not found for UPC: ${upc}`));
784
787
  if (result.error) {
@@ -806,7 +809,7 @@ program
806
809
  .action(async (options) => {
807
810
  try {
808
811
  let upcs = [];
809
-
812
+
810
813
  if (options.file) {
811
814
  const content = secureReadFile(options.file);
812
815
  upcs = content.split('\n').map(line => line.trim()).filter(line => line);
@@ -825,11 +828,11 @@ program
825
828
  const api = await getApiClient();
826
829
  const globalOpts = program.opts();
827
830
  const verbose = options.verbose || globalOpts.verbose;
828
-
831
+
829
832
  console.log(chalk.cyan(`šŸ” Batch searching ${upcs.length} UPCs...`));
830
-
833
+
831
834
  const result = await api.searchUPCBatch(upcs);
832
-
835
+
833
836
  if (program.opts().json) {
834
837
  console.log(JSON.stringify(result, null, 2));
835
838
  return;
@@ -839,7 +842,7 @@ program
839
842
  console.log(chalk.green(`āœ… Batch search complete!`));
840
843
  console.log(chalk.cyan(`šŸ“Š Found: ${result.summary.found}/${result.summary.total} products`));
841
844
  console.log('');
842
-
845
+
843
846
  // Results table
844
847
  const tableData = result.results.map(item => [
845
848
  item.success ? 'āœ…' : 'āŒ',
@@ -848,12 +851,12 @@ program
848
851
  item.success && item.product ? (item.product.brand || 'Unknown Brand') : 'N/A',
849
852
  item.success && item.product ? item.product.sources.join(', ') : 'N/A'
850
853
  ]);
851
-
854
+
852
855
  console.log(formatTable([
853
856
  ['Status', 'UPC', 'Product Name', 'Brand', 'Sources'],
854
857
  ...tableData
855
858
  ]));
856
-
859
+
857
860
  // Detailed results for found products (verbose mode)
858
861
  if (verbose) {
859
862
  const foundProducts = result.results.filter(r => r.success && r.product);
@@ -863,7 +866,7 @@ program
863
866
  foundProducts.forEach((item, index) => {
864
867
  const product = item.product;
865
868
  console.log(`\n${index + 1}. ${chalk.cyan(product.product_name || 'Unknown Product')} (${item.upc})`);
866
-
869
+
867
870
  if (product.brand) {
868
871
  console.log(` Brand: ${chalk.yellow(product.brand)}`);
869
872
  }
@@ -873,7 +876,7 @@ program
873
876
  if (product.serving_size) {
874
877
  console.log(` Serving: ${chalk.gray(product.serving_size)}`);
875
878
  }
876
-
879
+
877
880
  // Nutrition sample
878
881
  if (product.nutrition) {
879
882
  const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition;
@@ -887,12 +890,12 @@ program
887
890
  });
888
891
  }
889
892
  }
890
-
893
+
891
894
  // Performance summary
892
895
  if (result.processing_time_ms) {
893
896
  console.log(`\nā±ļø ${chalk.gray('Total Processing Time:')} ${formatResponseTime(result.processing_time_ms)}`);
894
897
  }
895
-
898
+
896
899
  } else {
897
900
  console.log(chalk.red(`āŒ ${result.error || 'Batch search failed'}`));
898
901
  process.exit(1);
@@ -911,7 +914,7 @@ program
911
914
  try {
912
915
  const api = await getApiClient(false); // Don't require auth for health check
913
916
  const result = await api.healthCheck();
914
-
917
+
915
918
  if (program.opts().json) {
916
919
  console.log(JSON.stringify(result, null, 2));
917
920
  return;
@@ -919,20 +922,20 @@ program
919
922
 
920
923
  console.log(chalk.green(`āœ… API Status: ${result.status}`));
921
924
  console.log(chalk.cyan(`šŸ”§ Version: ${result.version}`));
922
-
925
+
923
926
  if (result.services) {
924
927
  console.log(chalk.bold('šŸ”Œ Services:'));
925
928
  Object.entries(result.services).forEach(([service, status]) => {
926
- const icon = (status === 'available' || status === 'connected' || status.includes('total')) ? 'āœ…' :
927
- status === 'degraded' ? 'āš ļø' : 'āŒ';
929
+ const icon = (status === 'available' || status === 'connected' || status.includes('total')) ? 'āœ…' :
930
+ status === 'degraded' ? 'āš ļø' : 'āŒ';
928
931
  console.log(` ${icon} ${service}: ${status}`);
929
932
  });
930
933
  }
931
-
934
+
932
935
  if (result.performance) {
933
936
  console.log(chalk.bold('⚔ Performance:'));
934
937
  console.log(` šŸ“Š Avg Response: ${result.performance.avg_response_time_ms}ms`);
935
-
938
+
936
939
  if (result.performance.uptime) {
937
940
  console.log(` ā±ļø Uptime: ${result.performance.uptime}`);
938
941
  }
@@ -943,7 +946,7 @@ program
943
946
  console.log(` šŸ‘„ Active Users: ${result.performance.active_users}`);
944
947
  }
945
948
  }
946
-
949
+
947
950
  if (result.dashboard_data) {
948
951
  console.log(chalk.bold('šŸ“Š Dashboard Stats:'));
949
952
  if (result.dashboard_data.total_users) {
@@ -968,12 +971,12 @@ program
968
971
  // Helper function to get API client
969
972
  async function getApiClient(requireAuth = true) {
970
973
  const globalOpts = program.opts();
971
-
974
+
972
975
  let apiKey = globalOpts.apiKey;
973
-
976
+
974
977
  if (!apiKey && requireAuth) {
975
978
  apiKey = await auth.getApiKey();
976
-
979
+
977
980
  // Check if user is logged in (has JWT) but no API key - show helpful message
978
981
  if (!apiKey && auth.isLoggedIn()) {
979
982
  console.error(chalk.red('āŒ No API key found. You are logged in but need to create an API key.'));
@@ -984,7 +987,7 @@ async function getApiClient(requireAuth = true) {
984
987
  process.exit(1);
985
988
  }
986
989
  }
987
-
990
+
988
991
  return new NutritionAPI(apiKey, globalOpts.baseUrl, 30000, auth);
989
992
  }
990
993
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "avocavo",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Avocavo CLI - Nutrition analysis made simple. Get accurate USDA nutrition data with secure authentication.",
5
5
  "main": "index.js",
6
6
  "bin": {