avocavo 1.0.4 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  All notable changes to the Avocavo CLI will be documented in this file.
4
4
 
5
+ ## [1.1.1] - 2025-08-21
6
+
7
+ ### ✨ UPC/Barcode Search Support
8
+ - **NEW**: Complete UPC/barcode search functionality with `upc` and `upc-batch` commands
9
+ - **NEW**: Access to 4.4M+ product database from USDA Branded Foods + Open Food Facts
10
+ - **NEW**: Product information display with brand, ingredients, and nutrition data
11
+ - **NEW**: UPC health check with `upc-health` command
12
+
13
+ ### 🔧 UPC Command Features
14
+ - **ADDED**: `avocavo upc "041196912395"` - Single UPC lookup
15
+ - **ADDED**: `avocavo upc-batch -u "041196912395" "123456789012"` - Batch UPC processing
16
+ - **ADDED**: `avocavo upc-batch -f upcs.txt` - UPC search from file
17
+ - **IMPROVED**: Comprehensive product display with nutritional information
18
+ - **ENHANCED**: Response time reporting for UPC searches
19
+
20
+ ### 📚 API Integration
21
+ - **UPDATED**: NutritionAPI class with `searchUPC()` and `searchUPCBatch()` methods
22
+ - **IMPROVED**: Error handling for UPC service unavailability
23
+ - **ADDED**: UPC health monitoring and status reporting
24
+
25
+ ## [1.0.5] - 2025-08-04
26
+
27
+ ### 🎯 Login Logic Improvements
28
+ - **ENHANCED**: Improved login flow to always check server for all user API keys
29
+ - **FIXED**: Better handling of invalid selected API keys - shows specific "invalid key" message
30
+ - **ACCURATE**: Login now correctly identifies invalid keys vs missing keys vs multiple keys
31
+ - **SMART**: More intelligent key validation and user guidance after login
32
+
33
+ ### 🔧 Key Selection Logic
34
+ - **IMPROVED**: Always validates currently selected API key before showing account info
35
+ - **FALLBACK**: If selected key is invalid and user has 1 key, auto-selects the working key
36
+ - **GUIDANCE**: Clear differentiation between "invalid key" and "no keys" scenarios
37
+ - **ROBUST**: Better error handling for key validation failures
38
+
39
+ ### 🛠️ Technical Enhancements
40
+ - Consolidated login key detection into single comprehensive flow
41
+ - Added key validation testing before displaying account information
42
+ - Enhanced error messaging with specific guidance for each scenario
43
+ - Improved fallback handling for API key enumeration failures
44
+
5
45
  ## [1.0.4] - 2025-08-04
6
46
 
7
47
  ### 🔧 Login Experience Fixes
package/bin/avocavo.js CHANGED
@@ -5,7 +5,7 @@ const chalk = require('chalk');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
  const axios = require('axios');
8
- const { NutritionAPI } = require('../lib/api');
8
+ const { NutritionAPI, formatResponseTime } = require('../lib/api');
9
9
  const { AuthManager } = require('../lib/auth');
10
10
  const { formatNutrition, formatPerformanceMetrics, formatUSDAMatch, formatTable } = require('../lib/formatters');
11
11
  const { version } = require('../package.json');
@@ -78,79 +78,77 @@ program
78
78
  if (success) {
79
79
  console.log(chalk.green('✅ Successfully logged in!'));
80
80
 
81
- // Check for API keys - first check if user has a selected key
82
- let apiKey = await auth.getApiKey();
83
-
84
- if (apiKey) {
85
- // User has a selected API key, test it
86
- if (program.opts().verbose) {
87
- console.log(chalk.gray(`Debug: Using selected API key: ${apiKey.substring(0, 12)}...`));
88
- }
89
- const api = new NutritionAPI(apiKey, program.opts().baseUrl, 30000, auth);
90
- try {
91
- const account = await api.getAccountUsage();
92
- console.log(chalk.cyan(`📊 Account: ${account.email} (${account.api_tier} tier)`));
93
- console.log(chalk.cyan(`📈 Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`));
94
- } catch (err) {
95
- console.log(chalk.yellow('⚠️ Selected API key appears invalid'));
96
- console.log(chalk.cyan('💡 Try selecting a different key with: avocavo keys switch'));
97
- if (program.opts().verbose) {
98
- console.log(chalk.gray(`Debug: ${err.message}`));
99
- }
100
- }
101
- } else {
102
- // No selected API key, check if user has any keys at all
103
- try {
104
- const KeyManager = require('../lib/keys');
105
- const keyManager = new KeyManager(auth, program.opts().baseUrl);
81
+ // Check for API keys - always check server for all keys
82
+ try {
83
+ const KeyManager = require('../lib/keys');
84
+ const keyManager = new KeyManager(auth, program.opts().baseUrl);
85
+
86
+ // Get user's API keys from server
87
+ const headers = await keyManager.getAuthHeaders();
88
+ const response = await axios.get(`${program.opts().baseUrl}/api/keys`, { headers });
89
+
90
+ if (response.data.keys && response.data.keys.length > 0) {
91
+ const keyCount = response.data.keys.length;
106
92
 
107
- // Get user's API keys from server
108
- const headers = await keyManager.getAuthHeaders();
109
- const response = await axios.get(`${program.opts().baseUrl}/api/keys`, { headers });
93
+ // Check if user already has a selected key
94
+ let currentKey = await auth.getApiKey();
95
+ let keyWorked = false;
110
96
 
111
- if (response.data.keys && response.data.keys.length > 0) {
112
- const keyCount = response.data.keys.length;
113
-
114
- if (keyCount === 1) {
115
- // Auto-select the single key
116
- console.log(chalk.cyan('🔄 You have 1 API key. Auto-selecting it...'));
117
- const singleKey = response.data.keys[0];
118
-
119
- // Store the key as selected (simplified approach)
120
- await auth.storeApiKeySecurely(auth.getUserInfo().email, singleKey.api_key);
121
-
122
- // Test the auto-selected key
123
- const api = new NutritionAPI(singleKey.api_key, program.opts().baseUrl, 30000, auth);
124
- try {
125
- const account = await api.getAccountUsage();
126
- console.log(chalk.green('✅ API key activated!'));
127
- console.log(chalk.cyan(`📊 Account: ${account.email} (${account.api_tier} tier)`));
128
- console.log(chalk.cyan(`📈 Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`));
129
- } catch (err) {
130
- console.log(chalk.yellow('⚠️ Auto-selected API key appears invalid'));
131
- console.log(chalk.cyan('💡 Check your keys with: avocavo keys list'));
97
+ if (currentKey) {
98
+ // Test the current selected key
99
+ const api = new NutritionAPI(currentKey, program.opts().baseUrl, 30000, auth);
100
+ try {
101
+ const account = await api.getAccountUsage();
102
+ console.log(chalk.cyan(`📊 Account: ${account.email} (${account.api_tier} tier)`));
103
+ console.log(chalk.cyan(`📈 Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`));
104
+ keyWorked = true;
105
+ } catch (err) {
106
+ console.log(chalk.yellow('⚠️ Currently selected API key appears invalid'));
107
+ if (program.opts().verbose) {
108
+ console.log(chalk.gray(`Debug: ${err.message}`));
132
109
  }
133
- } else {
134
- // Multiple keys - prompt user to select one
135
- console.log(chalk.cyan(`💡 You have ${keyCount} API keys but none selected. Choose one to activate:`));
136
- console.log(chalk.cyan(' avocavo keys switch'));
137
- console.log(chalk.gray(' or'));
138
- console.log(chalk.cyan(' avocavo keys list'));
139
110
  }
140
- } else {
141
- // No API keys at all
142
- console.log(chalk.yellow('⚠️ You don\'t have any API keys yet'));
143
- console.log(chalk.cyan('💡 Create your first API key to start using the nutrition API:'));
144
- console.log(chalk.cyan(' avocavo keys create'));
145
111
  }
146
- } catch (keyCheckError) {
147
- // Fallback to create message if we can't check existing keys
148
- console.log(chalk.yellow('⚠️ Unable to check for existing API keys'));
149
- console.log(chalk.cyan('💡 Create an API key to start using the nutrition API:'));
150
- console.log(chalk.cyan(' avocavo keys create'));
151
- if (program.opts().verbose) {
152
- console.log(chalk.gray(`Debug: ${keyCheckError.message}`));
112
+
113
+ // If no working key and user has exactly 1 key, auto-select it
114
+ if (!keyWorked && keyCount === 1) {
115
+ console.log(chalk.cyan('🔄 Auto-selecting your API key...'));
116
+ const singleKey = response.data.keys[0];
117
+
118
+ // Store the key as selected
119
+ await auth.storeApiKeySecurely(auth.getUserInfo().email, singleKey.api_key);
120
+
121
+ // Test the auto-selected key
122
+ const api = new NutritionAPI(singleKey.api_key, program.opts().baseUrl, 30000, auth);
123
+ try {
124
+ const account = await api.getAccountUsage();
125
+ console.log(chalk.green('✅ API key activated!'));
126
+ console.log(chalk.cyan(`📊 Account: ${account.email} (${account.api_tier} tier)`));
127
+ console.log(chalk.cyan(`📈 Usage: ${account.usage.current_month}/${account.usage.monthly_limit || 'unlimited'}`));
128
+ } catch (err) {
129
+ console.log(chalk.yellow('⚠️ Auto-selected API key appears invalid'));
130
+ console.log(chalk.cyan('💡 Check your keys with: avocavo keys list'));
131
+ }
132
+ } else if (!keyWorked && keyCount > 1) {
133
+ // Multiple keys - prompt user to select one
134
+ console.log(chalk.cyan(`💡 You have ${keyCount} API keys. Choose one to activate:`));
135
+ console.log(chalk.cyan(' avocavo keys switch'));
136
+ console.log(chalk.gray(' or'));
137
+ console.log(chalk.cyan(' avocavo keys list'));
153
138
  }
139
+ } else {
140
+ // No API keys at all
141
+ console.log(chalk.yellow('⚠️ You don\'t have any API keys yet'));
142
+ console.log(chalk.cyan('💡 Create your first API key to start using the nutrition API:'));
143
+ console.log(chalk.cyan(' avocavo keys create'));
144
+ }
145
+ } catch (keyCheckError) {
146
+ // Fallback to create message if we can't check existing keys
147
+ console.log(chalk.yellow('⚠️ Unable to check for existing API keys'));
148
+ console.log(chalk.cyan('💡 Create an API key to start using the nutrition API:'));
149
+ console.log(chalk.cyan(' avocavo keys create'));
150
+ if (program.opts().verbose) {
151
+ console.log(chalk.gray(`Debug: ${keyCheckError.message}`));
154
152
  }
155
153
  }
156
154
  } else {
@@ -665,6 +663,246 @@ program
665
663
  }
666
664
  });
667
665
 
666
+ // UPC/Barcode search command
667
+ program
668
+ .command('upc <upc>')
669
+ .description('Search for product information by UPC/barcode')
670
+ .option('--verbose', 'Show detailed product information')
671
+ .action(async (upc, options) => {
672
+ try {
673
+ const api = await getApiClient();
674
+ const globalOpts = program.opts();
675
+ const verbose = options.verbose || globalOpts.verbose;
676
+
677
+ console.log(chalk.cyan(`🔍 Searching UPC: ${upc}...`));
678
+
679
+ const result = await api.searchUPC(upc);
680
+
681
+ if (program.opts().json) {
682
+ console.log(JSON.stringify(result, null, 2));
683
+ return;
684
+ }
685
+
686
+ if (result.success && result.product) {
687
+ const product = result.product;
688
+ console.log(chalk.green(`✅ Product Found!`));
689
+ console.log('');
690
+
691
+ // Basic product info
692
+ if (product.product_name) {
693
+ console.log(`📦 ${chalk.bold('Product:')} ${chalk.cyan(product.product_name)}`);
694
+ }
695
+ if (product.brand) {
696
+ console.log(`🏷️ ${chalk.bold('Brand:')} ${chalk.yellow(product.brand)}`);
697
+ }
698
+ if (product.manufacturer) {
699
+ console.log(`🏭 ${chalk.bold('Manufacturer:')} ${chalk.gray(product.manufacturer)}`);
700
+ }
701
+
702
+ // Data sources
703
+ if (product.sources && product.sources.length > 0) {
704
+ console.log(`📊 ${chalk.bold('Sources:')} ${chalk.magenta(product.sources.join(', '))}`);
705
+ }
706
+
707
+ // Categories
708
+ if (product.categories && product.categories.length > 0) {
709
+ const categoryDisplay = product.categories.slice(0, 3).join(', ');
710
+ const moreCategories = product.categories.length > 3 ? ` (+${product.categories.length - 3} more)` : '';
711
+ console.log(`📋 ${chalk.bold('Categories:')} ${chalk.blue(categoryDisplay)}${chalk.gray(moreCategories)}`);
712
+ }
713
+
714
+ // Serving info
715
+ if (product.serving_size) {
716
+ console.log(`🥄 ${chalk.bold('Serving Size:')} ${chalk.cyan(product.serving_size)}`);
717
+ }
718
+ if (product.servings_per_container) {
719
+ console.log(`📦 ${chalk.bold('Servings Per Container:')} ${chalk.cyan(product.servings_per_container)}`);
720
+ }
721
+
722
+ // Nutrition data sample (if available)
723
+ if (product.nutrition && typeof product.nutrition === 'object' && Object.keys(product.nutrition).length > 0) {
724
+ console.log('');
725
+ console.log(chalk.bold('🍎 Nutrition Data Available:'));
726
+
727
+ // Show sample from merged nutrition if available
728
+ const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition;
729
+ if (nutritionData && typeof nutritionData === 'object') {
730
+ const sampleNutrients = Object.entries(nutritionData)
731
+ .slice(0, 5)
732
+ .filter(([key, value]) => value !== null && value !== undefined);
733
+
734
+ sampleNutrients.forEach(([nutrient, value]) => {
735
+ console.log(` ${chalk.gray(nutrient.replace(/_/g, ' ').toUpperCase())}:`, chalk.green(value));
736
+ });
737
+
738
+ const totalNutrients = Object.keys(nutritionData).length;
739
+ if (totalNutrients > 5) {
740
+ console.log(` ${chalk.gray(`... and ${totalNutrients - 5} more nutrients`)}`);
741
+ }
742
+ }
743
+ }
744
+
745
+ // Verbose information
746
+ if (verbose) {
747
+ console.log('');
748
+ console.log(chalk.bold('📝 Additional Details:'));
749
+
750
+ if (product.ingredients_text) {
751
+ const ingredientsPreview = product.ingredients_text.length > 100 ?
752
+ product.ingredients_text.substring(0, 100) + '...' : product.ingredients_text;
753
+ console.log(` ${chalk.bold('Ingredients:')} ${chalk.gray(ingredientsPreview)}`);
754
+ }
755
+
756
+ if (product.packaging) {
757
+ console.log(` ${chalk.bold('Packaging:')} ${chalk.gray(product.packaging)}`);
758
+ }
759
+
760
+ if (product.countries && product.countries.length > 0) {
761
+ console.log(` ${chalk.bold('Countries:')} ${chalk.gray(product.countries.join(', '))}`);
762
+ }
763
+
764
+ if (product.quality_score) {
765
+ console.log(` ${chalk.bold('Quality Score:')} ${chalk.cyan(product.quality_score)}`);
766
+ }
767
+
768
+ if (product.images && product.images.length > 0) {
769
+ console.log(` ${chalk.bold('Images:')} ${chalk.cyan(product.images.length)} available`);
770
+ }
771
+ }
772
+
773
+ // Performance info
774
+ if (result.processing_time_ms) {
775
+ console.log('');
776
+ console.log(`⏱️ ${chalk.gray('Processing Time:')} ${formatResponseTime(result.processing_time_ms)}`);
777
+ }
778
+ if (result.from_cache) {
779
+ console.log(`💾 ${chalk.gray('Source:')} ${chalk.green('Cache (fast response)')}`);
780
+ }
781
+
782
+ } else {
783
+ console.log(chalk.yellow(`❌ Product not found for UPC: ${upc}`));
784
+ if (result.error) {
785
+ console.log(chalk.gray(`Error: ${result.error}`));
786
+ }
787
+ console.log('');
788
+ console.log(chalk.cyan('💡 Tips:'));
789
+ console.log(chalk.gray(' • Check that the UPC is correct (12-13 digits)'));
790
+ console.log(chalk.gray(' • Try removing any leading zeros'));
791
+ console.log(chalk.gray(' • Some products may not be in our 4.4M+ product database'));
792
+ }
793
+ } catch (error) {
794
+ console.error(chalk.red(`❌ UPC search error: ${error.message}`));
795
+ process.exit(1);
796
+ }
797
+ });
798
+
799
+ // UPC batch search command
800
+ program
801
+ .command('upc-batch')
802
+ .description('Search multiple UPCs/barcodes efficiently')
803
+ .option('-u, --upcs <upcs...>', 'UPCs to search')
804
+ .option('-f, --file <file>', 'Read UPCs from file (one per line)')
805
+ .option('--verbose', 'Show detailed results')
806
+ .action(async (options) => {
807
+ try {
808
+ let upcs = [];
809
+
810
+ if (options.file) {
811
+ const content = secureReadFile(options.file);
812
+ upcs = content.split('\n').map(line => line.trim()).filter(line => line);
813
+ } else if (options.upcs) {
814
+ upcs = options.upcs;
815
+ } else {
816
+ console.log(chalk.red('❌ Please provide UPCs via --upcs or --file'));
817
+ process.exit(1);
818
+ }
819
+
820
+ if (upcs.length === 0) {
821
+ console.log(chalk.red('❌ No UPCs provided'));
822
+ process.exit(1);
823
+ }
824
+
825
+ const api = await getApiClient();
826
+ const globalOpts = program.opts();
827
+ const verbose = options.verbose || globalOpts.verbose;
828
+
829
+ console.log(chalk.cyan(`🔍 Batch searching ${upcs.length} UPCs...`));
830
+
831
+ const result = await api.searchUPCBatch(upcs);
832
+
833
+ if (program.opts().json) {
834
+ console.log(JSON.stringify(result, null, 2));
835
+ return;
836
+ }
837
+
838
+ if (result.success) {
839
+ console.log(chalk.green(`✅ Batch search complete!`));
840
+ console.log(chalk.cyan(`📊 Found: ${result.summary.found}/${result.summary.total} products`));
841
+ console.log('');
842
+
843
+ // Results table
844
+ const tableData = result.results.map(item => [
845
+ item.success ? '✅' : '❌',
846
+ item.upc,
847
+ item.success && item.product ? (item.product.product_name || 'Unknown Product') : 'Not Found',
848
+ item.success && item.product ? (item.product.brand || 'Unknown Brand') : 'N/A',
849
+ item.success && item.product ? item.product.sources.join(', ') : 'N/A'
850
+ ]);
851
+
852
+ console.log(formatTable([
853
+ ['Status', 'UPC', 'Product Name', 'Brand', 'Sources'],
854
+ ...tableData
855
+ ]));
856
+
857
+ // Detailed results for found products (verbose mode)
858
+ if (verbose) {
859
+ const foundProducts = result.results.filter(r => r.success && r.product);
860
+ if (foundProducts.length > 0) {
861
+ console.log('');
862
+ console.log(chalk.bold('📦 Product Details:'));
863
+ foundProducts.forEach((item, index) => {
864
+ const product = item.product;
865
+ console.log(`\n${index + 1}. ${chalk.cyan(product.product_name || 'Unknown Product')} (${item.upc})`);
866
+
867
+ if (product.brand) {
868
+ console.log(` Brand: ${chalk.yellow(product.brand)}`);
869
+ }
870
+ if (product.categories && product.categories.length > 0) {
871
+ console.log(` Categories: ${chalk.blue(product.categories.slice(0, 2).join(', '))}`);
872
+ }
873
+ if (product.serving_size) {
874
+ console.log(` Serving: ${chalk.gray(product.serving_size)}`);
875
+ }
876
+
877
+ // Nutrition sample
878
+ if (product.nutrition) {
879
+ const nutritionData = product.nutrition.merged || product.nutrition.usda || product.nutrition.openfoodfacts || product.nutrition;
880
+ if (nutritionData && typeof nutritionData === 'object') {
881
+ const calories = nutritionData.energy_kcal || nutritionData.calories;
882
+ const protein = nutritionData.protein;
883
+ if (calories) console.log(` Calories: ${chalk.green(calories)}`);
884
+ if (protein) console.log(` Protein: ${chalk.green(protein)}g`);
885
+ }
886
+ }
887
+ });
888
+ }
889
+ }
890
+
891
+ // Performance summary
892
+ if (result.processing_time_ms) {
893
+ console.log(`\n⏱️ ${chalk.gray('Total Processing Time:')} ${formatResponseTime(result.processing_time_ms)}`);
894
+ }
895
+
896
+ } else {
897
+ console.log(chalk.red(`❌ ${result.error || 'Batch search failed'}`));
898
+ process.exit(1);
899
+ }
900
+ } catch (error) {
901
+ console.error(chalk.red(`❌ UPC batch search error: ${error.message}`));
902
+ process.exit(1);
903
+ }
904
+ });
905
+
668
906
  // Health check command
669
907
  program
670
908
  .command('health')
@@ -750,6 +988,8 @@ async function getApiClient(requireAuth = true) {
750
988
  return new NutritionAPI(apiKey, globalOpts.baseUrl, 30000, auth);
751
989
  }
752
990
 
991
+ // Helper function already imported from api.js
992
+
753
993
  // Handle unknown commands
754
994
  program.on('command:*', () => {
755
995
  console.error(chalk.red(`❌ Unknown command: ${program.args.join(' ')}`));
@@ -774,6 +1014,10 @@ Examples:
774
1014
  $ avocavo ingredient "1 cup rice" -v # Include USDA verification URL
775
1015
  $ avocavo recipe -i "2 cups flour" "1 cup milk" -s 8 # Analyze recipe
776
1016
  $ avocavo batch -i "1 cup rice" "2 tbsp oil" "4 oz chicken" # Batch analysis
1017
+ $ avocavo upc "041196912395" # Search UPC/barcode
1018
+ $ avocavo upc "041196912395" --verbose # Detailed UPC product info
1019
+ $ avocavo upc-batch -u "041196912395" "123456789012" # Batch UPC search
1020
+ $ avocavo upc-batch -f upcs.txt # UPC search from file
777
1021
  $ avocavo health # Check API health
778
1022
 
779
1023
  Authentication:
package/lib/api.js CHANGED
@@ -1,262 +1,197 @@
1
1
  const axios = require('axios');
2
2
  const chalk = require('chalk');
3
3
 
4
- class ApiError extends Error {
5
- constructor(message, statusCode = null, response = null) {
6
- super(message);
7
- this.name = 'ApiError';
8
- this.statusCode = statusCode;
9
- this.response = response;
10
- }
11
- }
12
-
13
4
  class NutritionAPI {
14
- constructor(apiKey, baseUrl = 'https://app.avocavo.app', timeout = 30000, authManager = null) {
5
+ constructor(apiKey, baseUrl = 'https://app.avocavo.app', timeout = 30000, auth = null) {
15
6
  this.apiKey = apiKey;
16
- this.baseUrl = baseUrl.replace(/\/$/, '');
7
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
17
8
  this.timeout = timeout;
18
- this.authManager = authManager;
9
+ this.auth = auth;
19
10
 
20
- // Create axios instance
11
+ // Create axios instance with default configuration
21
12
  this.client = axios.create({
22
- baseURL: this.baseUrl,
23
13
  timeout: this.timeout,
24
14
  headers: {
25
15
  'Content-Type': 'application/json',
26
- 'User-Agent': 'avocavo-nutrition-cli/1.8.0'
27
- }
28
- });
29
-
30
- // Add API key to requests if provided
31
- if (this.apiKey) {
32
- this.client.defaults.headers['X-API-Key'] = this.apiKey;
33
- }
34
-
35
- // Add request interceptor to handle authentication
36
- this.client.interceptors.request.use(async (config) => {
37
- // Determine authentication method based on endpoint
38
- const endpoint = config.url;
39
-
40
- if (process.env.NODE_ENV === 'development' || process.env.AVOCAVO_DEBUG) {
41
- console.log(`[DEBUG] Request to ${endpoint} with API key: ${this.apiKey ? '[REDACTED]' : 'none'}`);
42
- // Sanitize headers before logging
43
- const sanitizedHeaders = { ...config.headers };
44
- if (sanitizedHeaders['X-API-Key']) sanitizedHeaders['X-API-Key'] = '[REDACTED]';
45
- if (sanitizedHeaders['Authorization']) sanitizedHeaders['Authorization'] = '[REDACTED]';
46
- console.log(`[DEBUG] Request headers:`, JSON.stringify(sanitizedHeaders, null, 2));
16
+ 'User-Agent': 'avocavo-cli/1.1.0'
47
17
  }
48
-
49
- if (endpoint.startsWith('/api/auth/')) {
50
- // JWT authentication for key management endpoints
51
- if (this.authManager) {
52
- const jwt = await this.authManager.getJwtToken();
53
- if (jwt) {
54
- config.headers['Authorization'] = `Bearer ${jwt}`;
55
- // Remove API key header for JWT endpoints
56
- delete config.headers['X-API-Key'];
57
- }
58
- }
59
- } else if (endpoint.startsWith('/api/v2/nutrition/') || endpoint.startsWith('/api/v1/nutrition/')) {
60
- // API key authentication for nutrition endpoints
61
- if (this.apiKey) {
62
- // Use API key from constructor (command line or direct)
63
- config.headers['X-API-Key'] = this.apiKey;
64
- } else if (this.authManager) {
65
- // Try to get selected API key from auth manager
66
- const apiKey = await this.authManager.getApiKey();
67
- if (apiKey) {
68
- config.headers['X-API-Key'] = apiKey;
69
- }
70
- }
71
- }
72
-
73
- return config;
74
18
  });
75
-
76
- // Response interceptor for error handling
77
- this.client.interceptors.response.use(
78
- response => response,
79
- error => {
80
- if (error.response) {
81
- const status = error.response.status;
82
- const data = error.response.data;
83
-
84
- let message = data?.error || `HTTP ${status}`;
85
-
86
- if (status === 401) {
87
- message = 'Invalid API key or authentication required';
88
- if (process.env.NODE_ENV === 'development' || process.env.AVOCAVO_DEBUG) {
89
- console.log(`[DEBUG] 401 Response:`, JSON.stringify(data, null, 2));
90
- }
91
- } else if (status === 402) {
92
- message = 'Trial expired or payment required';
93
- } else if (status === 403) {
94
- message = 'Feature not available on your plan';
95
- } else if (status === 429) {
96
- message = 'Rate limit exceeded';
97
- } else if (status >= 500) {
98
- message = 'Server error - please try again later';
99
- }
100
-
101
- throw new ApiError(message, status, data);
102
- } else if (error.request) {
103
- throw new ApiError('Connection error. Check your internet connection.');
104
- } else {
105
- throw new ApiError(`Request failed: ${error.message}`);
106
- }
107
- }
108
- );
109
- }
110
19
 
111
- async analyze(input, servings = null) {
112
- try {
113
- // Smart routing to proper structured endpoints based on input type
114
- if (typeof input === 'string') {
115
- // Single ingredient - use bulletproof V2 ingredient endpoint
116
- return await this.analyzeIngredient(input);
117
- } else if (Array.isArray(input)) {
118
- if (servings && servings > 1) {
119
- // Recipe with servings - use bulletproof V2 recipe endpoint
120
- return await this.analyzeRecipe(input, servings);
121
- } else {
122
- // Multiple ingredients without servings - use bulletproof V2 batch endpoint
123
- return await this.analyzeBatch(input);
124
- }
20
+ // Set authentication headers
21
+ if (this.apiKey) {
22
+ if (this.apiKey.startsWith('eyJ') || this.apiKey.includes('.')) {
23
+ // JWT token
24
+ this.client.defaults.headers['Authorization'] = `Bearer ${this.apiKey}`;
125
25
  } else {
126
- throw new Error('Input must be a string (ingredient) or array (recipe/batch)');
26
+ // API key
27
+ this.client.defaults.headers['X-API-Key'] = this.apiKey;
127
28
  }
128
- } catch (error) {
129
- throw error;
130
29
  }
131
30
  }
132
31
 
133
- async analyzeIngredient(ingredient, includeVerification = false, verbose = false) {
32
+ async analyzeIngredient(ingredient, verify = false, verbose = false) {
134
33
  try {
135
- const requestData = {
34
+ const response = await this.client.post(`${this.baseUrl}/api/v2/nutrition/ingredient`, {
136
35
  ingredient: ingredient
137
- };
138
-
139
- // Add verbose parameter if requested
140
- if (verbose) {
141
- requestData.verbose = true;
142
- }
143
-
144
- const response = await this.client.post('/api/v2/nutrition/ingredient', requestData);
145
-
146
- const result = response.data;
147
-
148
- // For backward compatibility, ensure verification URL is included if requested
149
- if (includeVerification && result.success && result.nutrition?.fdc_id) {
150
- result.verification_url = `https://fdc.nal.usda.gov/fdc-app.html#/food-details/${result.nutrition.fdc_id}`;
151
- }
152
-
153
- return result;
36
+ });
37
+
38
+ return response.data;
154
39
  } catch (error) {
155
- throw error;
40
+ throw this.handleError(error);
156
41
  }
157
42
  }
158
43
 
159
44
  async analyzeRecipe(ingredients, servings = 1, verbose = false) {
160
45
  try {
161
- const requestData = {
46
+ const response = await this.client.post(`${this.baseUrl}/api/v2/nutrition/recipe`, {
162
47
  ingredients: ingredients,
163
48
  servings: servings
164
- };
165
-
166
- // Add verbose parameter if requested
167
- if (verbose) {
168
- requestData.verbose = true;
169
- }
170
-
171
- const response = await this.client.post('/api/v2/nutrition/recipe', requestData);
172
-
49
+ });
50
+
173
51
  return response.data;
174
52
  } catch (error) {
175
- throw error;
53
+ throw this.handleError(error);
176
54
  }
177
55
  }
178
56
 
179
57
  async analyzeBatch(ingredients, verbose = false) {
180
58
  try {
181
- // The batch endpoint now supports both array of strings and array of objects
182
- const requestData = {
183
- ingredients: ingredients
184
- };
185
-
186
- // Add verbose parameter if requested
187
- if (verbose) {
188
- requestData.verbose = true;
189
- }
190
-
191
- const response = await this.client.post('/api/v2/nutrition/batch', requestData);
192
-
59
+ // Transform array of strings to array of objects expected by batch endpoint
60
+ const ingredientObjects = ingredients.map((ingredient, index) => ({
61
+ ingredient: ingredient,
62
+ id: `item_${index + 1}`
63
+ }));
64
+
65
+ const response = await this.client.post(`${this.baseUrl}/api/v2/nutrition/batch`, {
66
+ ingredients: ingredientObjects
67
+ });
68
+
193
69
  return response.data;
194
70
  } catch (error) {
195
- throw error;
71
+ throw this.handleError(error);
196
72
  }
197
73
  }
198
74
 
199
75
  async getAccountUsage() {
200
76
  try {
201
- const response = await this.client.get('/api/v2/nutrition/account/usage');
202
-
203
- // V2 endpoint returns correct format
204
- const data = response.data;
205
- return {
206
- email: data.account?.email || 'Unknown',
207
- api_tier: data.account?.api_tier || 'Unknown',
208
- subscription_status: data.account?.subscription_status || 'Unknown',
209
- usage: data.usage || {
210
- current_month: 0,
211
- monthly_limit: 1000,
212
- remaining: 1000,
213
- percentage_used: 0,
214
- reset_date: new Date().toISOString(),
215
- days_until_reset: 30
216
- },
217
- // Include detailed credit information
218
- credits: {
219
- total: data.usage?.total_credits || 0,
220
- trial: data.usage?.trial_credits || 0,
221
- monthly: data.usage?.monthly_credits || 0,
222
- paid: data.usage?.paid_credits || 0
223
- }
224
- };
77
+ const response = await this.client.get(`${this.baseUrl}/api/v2/nutrition/account/usage`);
78
+ return response.data;
79
+ } catch (error) {
80
+ throw this.handleError(error);
81
+ }
82
+ }
83
+
84
+ async healthCheck() {
85
+ try {
86
+ // Remove auth headers for health check
87
+ const headers = { ...this.client.defaults.headers };
88
+ delete headers['X-API-Key'];
89
+ delete headers['Authorization'];
90
+
91
+ const response = await this.client.get(`${this.baseUrl}/api/v2/nutrition/health`, {
92
+ headers: headers
93
+ });
94
+
95
+ return response.data;
225
96
  } catch (error) {
226
- throw error;
97
+ throw this.handleError(error);
227
98
  }
228
99
  }
229
100
 
230
- async verifyFdcId(fdcId) {
101
+ // NEW UPC FUNCTIONALITY
102
+ async searchUPC(upc) {
231
103
  try {
232
- const response = await this.client.get(`/api/v2/nutrition/nutrition/verify/${fdcId}`);
104
+ const response = await this.client.post(`${this.baseUrl}/api/v2/upc/ingredient`, {
105
+ upc: upc
106
+ });
107
+
233
108
  return response.data;
234
109
  } catch (error) {
235
- throw error;
110
+ throw this.handleError(error);
236
111
  }
237
112
  }
238
113
 
239
- async healthCheck() {
114
+ async searchUPCBatch(upcs) {
240
115
  try {
241
- // Basic health check (no auth required)
242
- const tempHeaders = { ...this.client.defaults.headers };
243
- delete this.client.defaults.headers['X-API-Key'];
244
-
245
- const response = await this.client.get('/health');
246
-
247
- // Restore headers
248
- this.client.defaults.headers = tempHeaders;
249
-
116
+ const response = await this.client.post(`${this.baseUrl}/api/v2/upc/batch`, {
117
+ upcs: upcs
118
+ });
119
+
250
120
  return response.data;
251
121
  } catch (error) {
252
- // Restore headers even on error
253
- if (this.apiKey) {
254
- this.client.defaults.headers['X-API-Key'] = this.apiKey;
122
+ throw this.handleError(error);
123
+ }
124
+ }
125
+
126
+ async upcHealthCheck() {
127
+ try {
128
+ // Remove auth headers for health check
129
+ const headers = { ...this.client.defaults.headers };
130
+ delete headers['X-API-Key'];
131
+ delete headers['Authorization'];
132
+
133
+ const response = await this.client.get(`${this.baseUrl}/api/upc/health`, {
134
+ headers: headers
135
+ });
136
+
137
+ return response.data;
138
+ } catch (error) {
139
+ throw this.handleError(error);
140
+ }
141
+ }
142
+
143
+ handleError(error) {
144
+ if (error.response) {
145
+ const status = error.response.status;
146
+ const data = error.response.data;
147
+
148
+ // Extract error message
149
+ let message = data?.error || data?.message || `HTTP ${status} Error`;
150
+
151
+ // Handle specific status codes
152
+ switch (status) {
153
+ case 401:
154
+ message = 'Invalid API key or authentication failed';
155
+ break;
156
+ case 402:
157
+ message = 'Payment required - upgrade your plan or add credits';
158
+ break;
159
+ case 403:
160
+ message = data?.error || 'Access denied - feature not available on your plan';
161
+ break;
162
+ case 429:
163
+ const limit = data?.limit;
164
+ const usage = data?.usage;
165
+ message = `Rate limit exceeded${limit ? ` (${usage}/${limit})` : ''}`;
166
+ break;
167
+ case 500:
168
+ case 502:
169
+ case 503:
170
+ message = 'Server error - please try again later';
171
+ break;
255
172
  }
256
- throw error;
173
+
174
+ const apiError = new Error(message);
175
+ apiError.status = status;
176
+ apiError.response = data;
177
+ return apiError;
178
+ } else if (error.request) {
179
+ return new Error('Network error - please check your connection');
180
+ } else {
181
+ return new Error(error.message || 'Unknown error occurred');
257
182
  }
258
183
  }
184
+ }
259
185
 
186
+ // Helper function to format response time
187
+ function formatResponseTime(ms) {
188
+ if (ms < 1000) {
189
+ return chalk.green(`${ms}ms`);
190
+ } else if (ms < 3000) {
191
+ return chalk.yellow(`${ms}ms`);
192
+ } else {
193
+ return chalk.red(`${ms}ms`);
194
+ }
260
195
  }
261
196
 
262
- module.exports = { NutritionAPI, ApiError };
197
+ module.exports = { NutritionAPI, formatResponseTime };
@@ -74,7 +74,7 @@ try {
74
74
  }
75
75
 
76
76
  class SupabaseAuthManager {
77
- constructor(baseUrl = 'https://app.avocavo.app') {
77
+ constructor(baseUrl = 'https://nutrition.avocavo.app') {
78
78
  this.baseUrl = baseUrl.replace(/\/$/, '');
79
79
  this.serviceName = 'avocavo-nutrition';
80
80
  this.keytarAvailable = keytarAvailable;
package/lib/auth.js CHANGED
@@ -17,7 +17,7 @@ try {
17
17
  }
18
18
 
19
19
  class AuthManager {
20
- constructor(baseUrl = 'https://app.avocavo.app') {
20
+ constructor(baseUrl = 'https://nutrition.avocavo.app') {
21
21
  this.baseUrl = baseUrl.replace(/\/$/, '');
22
22
  this.serviceName = 'avocavo-nutrition';
23
23
  this.keytarAvailable = keytarAvailable;
package/lib/keys.js CHANGED
@@ -5,7 +5,7 @@ const Table = require('cli-table3');
5
5
  const inquirer = require('inquirer');
6
6
 
7
7
  class KeyManager {
8
- constructor(auth, baseUrl = 'https://app.avocavo.app') {
8
+ constructor(auth, baseUrl = 'https://nutrition.avocavo.app') {
9
9
  this.auth = auth;
10
10
  this.baseUrl = baseUrl;
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "avocavo",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
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": {