avocavo 1.0.5 → 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,26 @@
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
+
5
25
  ## [1.0.5] - 2025-08-04
6
26
 
7
27
  ### 🎯 Login Logic Improvements
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');
@@ -663,6 +663,246 @@ program
663
663
  }
664
664
  });
665
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
+
666
906
  // Health check command
667
907
  program
668
908
  .command('health')
@@ -748,6 +988,8 @@ async function getApiClient(requireAuth = true) {
748
988
  return new NutritionAPI(apiKey, globalOpts.baseUrl, 30000, auth);
749
989
  }
750
990
 
991
+ // Helper function already imported from api.js
992
+
751
993
  // Handle unknown commands
752
994
  program.on('command:*', () => {
753
995
  console.error(chalk.red(`❌ Unknown command: ${program.args.join(' ')}`));
@@ -772,6 +1014,10 @@ Examples:
772
1014
  $ avocavo ingredient "1 cup rice" -v # Include USDA verification URL
773
1015
  $ avocavo recipe -i "2 cups flour" "1 cup milk" -s 8 # Analyze recipe
774
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
775
1021
  $ avocavo health # Check API health
776
1022
 
777
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.5",
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": {