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 +40 -0
- package/bin/avocavo.js +312 -68
- package/lib/api.js +135 -200
- package/lib/auth-supabase.js +1 -1
- package/lib/auth.js +1 -1
- package/lib/keys.js +1 -1
- package/package.json +1 -1
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 -
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
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
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
93
|
+
// Check if user already has a selected key
|
|
94
|
+
let currentKey = await auth.getApiKey();
|
|
95
|
+
let keyWorked = false;
|
|
110
96
|
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
console.log(chalk.cyan(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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,
|
|
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.
|
|
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-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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,
|
|
32
|
+
async analyzeIngredient(ingredient, verify = false, verbose = false) {
|
|
134
33
|
try {
|
|
135
|
-
const
|
|
34
|
+
const response = await this.client.post(`${this.baseUrl}/api/v2/nutrition/ingredient`, {
|
|
136
35
|
ingredient: ingredient
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
101
|
+
// NEW UPC FUNCTIONALITY
|
|
102
|
+
async searchUPC(upc) {
|
|
231
103
|
try {
|
|
232
|
-
const response = await this.client.
|
|
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
|
|
114
|
+
async searchUPCBatch(upcs) {
|
|
240
115
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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,
|
|
197
|
+
module.exports = { NutritionAPI, formatResponseTime };
|
package/lib/auth-supabase.js
CHANGED
|
@@ -74,7 +74,7 @@ try {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
class SupabaseAuthManager {
|
|
77
|
-
constructor(baseUrl = 'https://
|
|
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://
|
|
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://
|
|
8
|
+
constructor(auth, baseUrl = 'https://nutrition.avocavo.app') {
|
|
9
9
|
this.auth = auth;
|
|
10
10
|
this.baseUrl = baseUrl;
|
|
11
11
|
}
|