diff --git a/optional-skills/health/fitness-nutrition/SKILL.md b/optional-skills/health/fitness-nutrition/SKILL.md new file mode 100644 index 000000000..672f0ccd0 --- /dev/null +++ b/optional-skills/health/fitness-nutrition/SKILL.md @@ -0,0 +1,255 @@ +--- +name: fitness-nutrition +description: > + Gym workout planner and nutrition tracker. Search 690+ exercises by muscle, + equipment, or category via wger. Look up macros and calories for 380,000+ + foods via USDA FoodData Central. Compute BMI, TDEE, one-rep max, macro + splits, and body fat — pure Python, no pip installs. Built for anyone + chasing gains, cutting weight, or just trying to eat better. +version: 1.0.0 +authors: + - haileymarshall +license: MIT +metadata: + hermes: + tags: [health, fitness, nutrition, gym, workout, diet, exercise] + category: health + prerequisites: + commands: [curl, python3] +required_environment_variables: + - name: USDA_API_KEY + prompt: "USDA FoodData Central API key (free)" + help: "Get one free at https://fdc.nal.usda.gov/api-key-signup/ — or skip to use DEMO_KEY with lower rate limits" + required_for: "higher rate limits on food/nutrition lookups (DEMO_KEY works without signup)" + optional: true +--- + +# Fitness & Nutrition + +Expert fitness coach and sports nutritionist skill. Two data sources +plus offline calculators — everything a gym-goer needs in one place. + +**Data sources (all free, no pip dependencies):** + +- **wger** (https://wger.de/api/v2/) — open exercise database, 690+ exercises with muscles, equipment, images. Public endpoints need zero authentication. +- **USDA FoodData Central** (https://api.nal.usda.gov/fdc/v1/) — US government nutrition database, 380,000+ foods. `DEMO_KEY` works instantly; free signup for higher limits. + +**Offline calculators (pure stdlib Python):** + +- BMI, TDEE (Mifflin-St Jeor), one-rep max (Epley/Brzycki/Lombardi), macro splits, body fat % (US Navy method) + +--- + +## When to Use + +Trigger this skill when the user asks about: +- Exercises, workouts, gym routines, muscle groups, workout splits +- Food macros, calories, protein content, meal planning, calorie counting +- Body composition: BMI, body fat, TDEE, caloric surplus/deficit +- One-rep max estimates, training percentages, progressive overload +- Macro ratios for cutting, bulking, or maintenance + +--- + +## Procedure + +### Exercise Lookup (wger API) + +All wger public endpoints return JSON and require no auth. Always add +`format=json` and `language=2` (English) to exercise queries. + +**Step 1 — Identify what the user wants:** + +- By muscle → use `/api/v2/exercise/?muscles={id}&language=2&status=2&format=json` +- By category → use `/api/v2/exercise/?category={id}&language=2&status=2&format=json` +- By equipment → use `/api/v2/exercise/?equipment={id}&language=2&status=2&format=json` +- By name → use `/api/v2/exercise/search/?term={query}&language=english&format=json` +- Full details → use `/api/v2/exerciseinfo/{exercise_id}/?format=json` + +**Step 2 — Reference IDs (so you don't need extra API calls):** + +Exercise categories: + +| ID | Category | +|----|-------------| +| 8 | Arms | +| 9 | Legs | +| 10 | Abs | +| 11 | Chest | +| 12 | Back | +| 13 | Shoulders | +| 14 | Calves | +| 15 | Cardio | + +Muscles: + +| ID | Muscle | ID | Muscle | +|----|---------------------------|----|-------------------------| +| 1 | Biceps brachii | 2 | Anterior deltoid | +| 3 | Serratus anterior | 4 | Pectoralis major | +| 5 | Obliquus externus | 6 | Gastrocnemius | +| 7 | Rectus abdominis | 8 | Gluteus maximus | +| 9 | Trapezius | 10 | Quadriceps femoris | +| 11 | Biceps femoris | 12 | Latissimus dorsi | +| 13 | Brachialis | 14 | Triceps brachii | +| 15 | Soleus | | | + +Equipment: + +| ID | Equipment | +|----|----------------| +| 1 | Barbell | +| 3 | Dumbbell | +| 4 | Gym mat | +| 5 | Swiss Ball | +| 6 | Pull-up bar | +| 7 | none (bodyweight) | +| 8 | Bench | +| 9 | Incline bench | +| 10 | Kettlebell | + +**Step 3 — Fetch and present results:** + +```bash +# Search exercises by name +QUERY="$1" +ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$QUERY") +curl -s "https://wger.de/api/v2/exercise/search/?term=${ENCODED}&language=english&format=json" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +for s in data.get('suggestions',[])[:10]: + d=s.get('data',{}) + print(f\" ID {d.get('id','?'):>4} | {d.get('name','N/A'):<35} | Category: {d.get('category','N/A')}\") +" +``` + +```bash +# Get full details for a specific exercise +EXERCISE_ID="$1" +curl -s "https://wger.de/api/v2/exerciseinfo/${EXERCISE_ID}/?format=json" \ + | python3 -c " +import json,sys,html,re +data=json.load(sys.stdin) +trans=[t for t in data.get('translations',[]) if t.get('language')==2] +t=trans[0] if trans else data.get('translations',[{}])[0] +desc=re.sub('<[^>]+>','',html.unescape(t.get('description','N/A'))) +print(f\"Exercise : {t.get('name','N/A')}\") +print(f\"Category : {data.get('category',{}).get('name','N/A')}\") +print(f\"Primary : {', '.join(m.get('name_en','') for m in data.get('muscles',[])) or 'N/A'}\") +print(f\"Secondary : {', '.join(m.get('name_en','') for m in data.get('muscles_secondary',[])) or 'none'}\") +print(f\"Equipment : {', '.join(e.get('name','') for e in data.get('equipment',[])) or 'bodyweight'}\") +print(f\"How to : {desc[:500]}\") +imgs=data.get('images',[]) +if imgs: print(f\"Image : {imgs[0].get('image','')}\") +" +``` + +```bash +# List exercises filtering by muscle, category, or equipment +# Combine filters as needed: ?muscles=4&equipment=1&language=2&status=2 +FILTER="$1" # e.g. "muscles=4" or "category=11" or "equipment=3" +curl -s "https://wger.de/api/v2/exercise/?${FILTER}&language=2&status=2&limit=20&format=json" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +print(f'Found {data.get(\"count\",0)} exercises.') +for ex in data.get('results',[]): + print(f\" ID {ex['id']:>4} | muscles: {ex.get('muscles',[])} | equipment: {ex.get('equipment',[])}\") +" +``` + +### Nutrition Lookup (USDA FoodData Central) + +Uses `USDA_API_KEY` env var if set, otherwise falls back to `DEMO_KEY`. +DEMO_KEY = 30 requests/hour. Free signup key = 1,000 requests/hour. + +```bash +# Search foods by name +FOOD="$1" +API_KEY="${USDA_API_KEY:-DEMO_KEY}" +ENCODED=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$FOOD") +curl -s "https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${API_KEY}&query=${ENCODED}&pageSize=5&dataType=Foundation,SR%20Legacy" \ + | python3 -c " +import json,sys +data=json.load(sys.stdin) +foods=data.get('foods',[]) +if not foods: print('No foods found.'); sys.exit() +for f in foods: + n={x['nutrientName']:x.get('value','?') for x in f.get('foodNutrients',[])} + cal=n.get('Energy','?'); prot=n.get('Protein','?') + fat=n.get('Total lipid (fat)','?'); carb=n.get('Carbohydrate, by difference','?') + print(f\"{f.get('description','N/A')}\") + print(f\" Per 100g: {cal} kcal | {prot}g protein | {fat}g fat | {carb}g carbs\") + print(f\" FDC ID: {f.get('fdcId','N/A')}\") + print() +" +``` + +```bash +# Detailed nutrient profile by FDC ID +FDC_ID="$1" +API_KEY="${USDA_API_KEY:-DEMO_KEY}" +curl -s "https://api.nal.usda.gov/fdc/v1/food/${FDC_ID}?api_key=${API_KEY}" \ + | python3 -c " +import json,sys +d=json.load(sys.stdin) +print(f\"Food: {d.get('description','N/A')}\") +print(f\"{'Nutrient':<40} {'Amount':>8} {'Unit'}\") +print('-'*56) +for x in sorted(d.get('foodNutrients',[]),key=lambda x:x.get('nutrient',{}).get('rank',9999)): + nut=x.get('nutrient',{}); amt=x.get('amount',0) + if amt and float(amt)>0: + print(f\" {nut.get('name',''):<38} {amt:>8} {nut.get('unitName','')}\") +" +``` + +### Offline Calculators + +Use the helper scripts in `scripts/` for batch operations, +or run inline for single calculations: + +- `python3 scripts/body_calc.py bmi ` +- `python3 scripts/body_calc.py tdee ` +- `python3 scripts/body_calc.py 1rm ` +- `python3 scripts/body_calc.py macros ` +- `python3 scripts/body_calc.py bodyfat [hip_cm] ` + +See `references/FORMULAS.md` for the science behind each formula. + +--- + +## Pitfalls + +- wger exercise endpoint returns **all languages by default** — always add `language=2` for English +- wger includes **unverified user submissions** — add `status=2` to only get approved exercises +- USDA `DEMO_KEY` has **30 req/hour** — add `sleep 2` between batch requests or get a free key +- USDA data is **per 100g** — remind users to scale to their actual portion size +- BMI does not distinguish muscle from fat — high BMI in muscular people is not necessarily unhealthy +- Body fat formulas are **estimates** (±3-5%) — recommend DEXA scans for precision +- 1RM formulas lose accuracy above 10 reps — use sets of 3-5 for best estimates +- wger's `exercise/search` endpoint uses `term` not `query` as the parameter name + +--- + +## Verification + +After running exercise search: confirm results include exercise names, muscle groups, and equipment. +After nutrition lookup: confirm per-100g macros are returned with kcal, protein, fat, carbs. +After calculators: sanity-check outputs (e.g. TDEE should be 1500-3500 for most adults). + +--- + +## Quick Reference + +| Task | Source | Endpoint | +|------|--------|----------| +| Search exercises by name | wger | `GET /api/v2/exercise/search/?term=&language=english` | +| Exercise details | wger | `GET /api/v2/exerciseinfo/{id}/` | +| Filter by muscle | wger | `GET /api/v2/exercise/?muscles={id}&language=2&status=2` | +| Filter by equipment | wger | `GET /api/v2/exercise/?equipment={id}&language=2&status=2` | +| List categories | wger | `GET /api/v2/exercisecategory/` | +| List muscles | wger | `GET /api/v2/muscle/` | +| Search foods | USDA | `GET /fdc/v1/foods/search?query=&dataType=Foundation,SR Legacy` | +| Food details | USDA | `GET /fdc/v1/food/{fdcId}` | +| BMI / TDEE / 1RM / macros | offline | `python3 scripts/body_calc.py` | \ No newline at end of file diff --git a/optional-skills/health/fitness-nutrition/references/FORMULAS.md b/optional-skills/health/fitness-nutrition/references/FORMULAS.md new file mode 100644 index 000000000..763c0b3a1 --- /dev/null +++ b/optional-skills/health/fitness-nutrition/references/FORMULAS.md @@ -0,0 +1,100 @@ +# Formulas Reference + +Scientific references for all calculators used in the fitness-nutrition skill. + +## BMI (Body Mass Index) + +**Formula:** BMI = weight (kg) / height (m)² + +| Category | BMI Range | +|-------------|------------| +| Underweight | < 18.5 | +| Normal | 18.5 – 24.9 | +| Overweight | 25.0 – 29.9 | +| Obese | 30.0+ | + +**Limitation:** BMI does not distinguish muscle from fat. A muscular person +can have a high BMI while being lean. Use body fat % for a better picture. + +Reference: Quetelet, A. (1832). Keys et al., Int J Obes (1972). + +## TDEE (Total Daily Energy Expenditure) + +Uses the **Mifflin-St Jeor equation** — the most accurate BMR predictor for +the general population according to the ADA (2005). + +**BMR formulas:** + +- Male: BMR = 10 × weight(kg) + 6.25 × height(cm) − 5 × age + 5 +- Female: BMR = 10 × weight(kg) + 6.25 × height(cm) − 5 × age − 161 + +**Activity multipliers:** + +| Level | Description | Multiplier | +|-------|--------------------------------|------------| +| 1 | Sedentary (desk job) | 1.200 | +| 2 | Lightly active (1-3 days/wk) | 1.375 | +| 3 | Moderately active (3-5 days) | 1.550 | +| 4 | Very active (6-7 days) | 1.725 | +| 5 | Extremely active (2x/day) | 1.900 | + +Reference: Mifflin et al., Am J Clin Nutr 51, 241-247 (1990). + +## One-Rep Max (1RM) + +Three validated formulas. Average of all three is most reliable. + +- **Epley:** 1RM = w × (1 + r/30) +- **Brzycki:** 1RM = w × 36 / (37 − r) +- **Lombardi:** 1RM = w × r^0.1 + +All formulas are most accurate for r ≤ 10. Above 10 reps, error increases. + +Reference: LeSuer et al., J Strength Cond Res 11(4), 211-213 (1997). + +## Macro Splits + +Recommended splits based on goal: + +| Goal | Protein | Fat | Carbs | Calorie Offset | +|-------------|---------|------|-------|----------------| +| Fat loss | 40% | 30% | 30% | −500 kcal | +| Maintenance | 30% | 30% | 40% | 0 | +| Lean bulk | 30% | 25% | 45% | +400 kcal | + +Protein targets for muscle growth: 1.6–2.2 g/kg body weight per day. +Minimum fat intake: 0.5 g/kg to support hormone production. + +Conversion: Protein = 4 kcal/g, Fat = 9 kcal/g, Carbs = 4 kcal/g. + +Reference: Morton et al., Br J Sports Med 52, 376–384 (2018). + +## Body Fat % (US Navy Method) + +**Male:** + +BF% = 86.010 × log₁₀(waist − neck) − 70.041 × log₁₀(height) + 36.76 + +**Female:** + +BF% = 163.205 × log₁₀(waist + hip − neck) − 97.684 × log₁₀(height) − 78.387 + +All measurements in centimeters. + +| Category | Male | Female | +|--------------|--------|--------| +| Essential | 2-5% | 10-13% | +| Athletic | 6-13% | 14-20% | +| Fitness | 14-17% | 21-24% | +| Average | 18-24% | 25-31% | +| Obese | 25%+ | 32%+ | + +Accuracy: ±3-5% compared to DEXA. Measure at the navel (waist), +at the Adam's apple (neck), and widest point (hip, females only). + +Reference: Hodgdon & Beckett, Naval Health Research Center (1984). + +## APIs + +- wger: https://wger.de/api/v2/ — AGPL-3.0, exercise data is CC-BY-SA 3.0 +- USDA FoodData Central: https://api.nal.usda.gov/fdc/v1/ — public domain (CC0 1.0) \ No newline at end of file diff --git a/optional-skills/health/fitness-nutrition/scripts/body_calc.py b/optional-skills/health/fitness-nutrition/scripts/body_calc.py new file mode 100644 index 000000000..2d07129ce --- /dev/null +++ b/optional-skills/health/fitness-nutrition/scripts/body_calc.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +""" +body_calc.py — All-in-one fitness calculator. + +Subcommands: + bmi + tdee + 1rm + macros + bodyfat [hip_cm] + +No external dependencies — stdlib only. +""" +import sys +import math + + +def bmi(weight_kg, height_cm): + h = height_cm / 100 + val = weight_kg / (h * h) + if val < 18.5: + cat = "Underweight" + elif val < 25: + cat = "Normal weight" + elif val < 30: + cat = "Overweight" + else: + cat = "Obese" + print(f"BMI: {val:.1f} — {cat}") + print() + print("Ranges:") + print(f" Underweight : < 18.5") + print(f" Normal : 18.5 – 24.9") + print(f" Overweight : 25.0 – 29.9") + print(f" Obese : 30.0+") + + +def tdee(weight_kg, height_cm, age, sex, activity): + if sex.upper() == "M": + bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age + 5 + else: + bmr = 10 * weight_kg + 6.25 * height_cm - 5 * age - 161 + + multipliers = { + 1: ("Sedentary (desk job, no exercise)", 1.2), + 2: ("Lightly active (1-3 days/week)", 1.375), + 3: ("Moderately active (3-5 days/week)", 1.55), + 4: ("Very active (6-7 days/week)", 1.725), + 5: ("Extremely active (athlete + physical job)", 1.9), + } + + label, mult = multipliers.get(activity, ("Moderate", 1.55)) + total = bmr * mult + + print(f"BMR (Mifflin-St Jeor): {bmr:.0f} kcal/day") + print(f"Activity: {label} (x{mult})") + print(f"TDEE: {total:.0f} kcal/day") + print() + print("Calorie targets:") + print(f" Aggressive cut (-750): {total - 750:.0f} kcal/day") + print(f" Fat loss (-500): {total - 500:.0f} kcal/day") + print(f" Mild cut (-250): {total - 250:.0f} kcal/day") + print(f" Maintenance : {total:.0f} kcal/day") + print(f" Lean bulk (+250): {total + 250:.0f} kcal/day") + print(f" Bulk (+500): {total + 500:.0f} kcal/day") + + +def one_rep_max(weight, reps): + if reps < 1: + print("Error: reps must be at least 1.") + sys.exit(1) + if reps == 1: + print(f"1RM = {weight:.1f} (actual single)") + return + + epley = weight * (1 + reps / 30) + brzycki = weight * (36 / (37 - reps)) if reps < 37 else 0 + lombardi = weight * (reps ** 0.1) + avg = (epley + brzycki + lombardi) / 3 + + print(f"Estimated 1RM ({weight} x {reps} reps):") + print(f" Epley : {epley:.1f}") + print(f" Brzycki : {brzycki:.1f}") + print(f" Lombardi : {lombardi:.1f}") + print(f" Average : {avg:.1f}") + print() + print("Training percentages off average 1RM:") + for pct, rep_range in [ + (100, "1"), (95, "1-2"), (90, "3-4"), (85, "4-6"), + (80, "6-8"), (75, "8-10"), (70, "10-12"), + (65, "12-15"), (60, "15-20"), + ]: + print(f" {pct:>3}% = {avg * pct / 100:>7.1f} (~{rep_range} reps)") + + +def macros(tdee_kcal, goal): + goal = goal.lower() + if goal in ("cut", "lose", "deficit"): + cals = tdee_kcal - 500 + p, f, c = 0.40, 0.30, 0.30 + label = "Fat Loss (-500 kcal)" + elif goal in ("bulk", "gain", "surplus"): + cals = tdee_kcal + 400 + p, f, c = 0.30, 0.25, 0.45 + label = "Lean Bulk (+400 kcal)" + else: + cals = tdee_kcal + p, f, c = 0.30, 0.30, 0.40 + label = "Maintenance" + + prot_g = cals * p / 4 + fat_g = cals * f / 9 + carb_g = cals * c / 4 + + print(f"Goal: {label}") + print(f"Daily calories: {cals:.0f} kcal") + print() + print(f" Protein : {prot_g:>6.0f}g ({p * 100:.0f}%) = {prot_g * 4:.0f} kcal") + print(f" Fat : {fat_g:>6.0f}g ({f * 100:.0f}%) = {fat_g * 9:.0f} kcal") + print(f" Carbs : {carb_g:>6.0f}g ({c * 100:.0f}%) = {carb_g * 4:.0f} kcal") + print() + print(f"Per meal (3 meals): P {prot_g / 3:.0f}g | F {fat_g / 3:.0f}g | C {carb_g / 3:.0f}g") + print(f"Per meal (4 meals): P {prot_g / 4:.0f}g | F {fat_g / 4:.0f}g | C {carb_g / 4:.0f}g") + + +def bodyfat(sex, neck_cm, waist_cm, hip_cm, height_cm): + sex = sex.upper() + if sex == "M": + if waist_cm <= neck_cm: + print("Error: waist must be larger than neck."); sys.exit(1) + bf = 86.010 * math.log10(waist_cm - neck_cm) - 70.041 * math.log10(height_cm) + 36.76 + else: + if (waist_cm + hip_cm) <= neck_cm: + print("Error: waist + hip must be larger than neck."); sys.exit(1) + bf = 163.205 * math.log10(waist_cm + hip_cm - neck_cm) - 97.684 * math.log10(height_cm) - 78.387 + + print(f"Estimated body fat: {bf:.1f}%") + + if sex == "M": + ranges = [ + (6, "Essential fat (2-5%)"), + (14, "Athletic (6-13%)"), + (18, "Fitness (14-17%)"), + (25, "Average (18-24%)"), + ] + default = "Obese (25%+)" + else: + ranges = [ + (14, "Essential fat (10-13%)"), + (21, "Athletic (14-20%)"), + (25, "Fitness (21-24%)"), + (32, "Average (25-31%)"), + ] + default = "Obese (32%+)" + + cat = default + for threshold, label in ranges: + if bf < threshold: + cat = label + break + + print(f"Category: {cat}") + print(f"Method: US Navy circumference formula") + + +def usage(): + print(__doc__) + sys.exit(1) + + +def main(): + if len(sys.argv) < 2: + usage() + + cmd = sys.argv[1].lower() + + try: + if cmd == "bmi": + bmi(float(sys.argv[2]), float(sys.argv[3])) + + elif cmd == "tdee": + tdee( + float(sys.argv[2]), float(sys.argv[3]), + int(sys.argv[4]), sys.argv[5], int(sys.argv[6]), + ) + + elif cmd in ("1rm", "orm"): + one_rep_max(float(sys.argv[2]), int(sys.argv[3])) + + elif cmd == "macros": + macros(float(sys.argv[2]), sys.argv[3]) + + elif cmd == "bodyfat": + sex = sys.argv[2] + if sex.upper() == "M": + bodyfat(sex, float(sys.argv[3]), float(sys.argv[4]), 0, float(sys.argv[5])) + else: + bodyfat(sex, float(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5]), float(sys.argv[6])) + + else: + print(f"Unknown command: {cmd}") + usage() + + except (IndexError, ValueError) as e: + print(f"Error: {e}") + usage() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/optional-skills/health/fitness-nutrition/scripts/nutrition_search.py b/optional-skills/health/fitness-nutrition/scripts/nutrition_search.py new file mode 100644 index 000000000..7494f6c38 --- /dev/null +++ b/optional-skills/health/fitness-nutrition/scripts/nutrition_search.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +""" +nutrition_search.py — Search USDA FoodData Central for nutrition info. + +Usage: + python3 nutrition_search.py "chicken breast" + python3 nutrition_search.py "rice" "eggs" "broccoli" + echo -e "oats\\nbanana\\nwhey protein" | python3 nutrition_search.py - + +Reads USDA_API_KEY from environment, falls back to DEMO_KEY. +No external dependencies. +""" +import sys +import os +import json +import time +import urllib.request +import urllib.parse +import urllib.error + +API_KEY = os.environ.get("USDA_API_KEY", "DEMO_KEY") +BASE = "https://api.nal.usda.gov/fdc/v1" + + +def search(query, max_results=3): + encoded = urllib.parse.quote(query) + url = ( + f"{BASE}/foods/search?api_key={API_KEY}" + f"&query={encoded}&pageSize={max_results}" + f"&dataType=Foundation,SR%20Legacy" + ) + try: + req = urllib.request.Request(url, headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=15) as r: + return json.loads(r.read()) + except Exception as e: + print(f" API error: {e}", file=sys.stderr) + return None + + +def display(food): + nutrients = {n["nutrientName"]: n.get("value", "?") for n in food.get("foodNutrients", [])} + cal = nutrients.get("Energy", "?") + prot = nutrients.get("Protein", "?") + fat = nutrients.get("Total lipid (fat)", "?") + carb = nutrients.get("Carbohydrate, by difference", "?") + fib = nutrients.get("Fiber, total dietary", "?") + sug = nutrients.get("Sugars, total including NLEA", "?") + + print(f" {food.get('description', 'N/A')}") + print(f" Calories : {cal} kcal") + print(f" Protein : {prot}g") + print(f" Fat : {fat}g") + print(f" Carbs : {carb}g (fiber: {fib}g, sugar: {sug}g)") + print(f" FDC ID : {food.get('fdcId', 'N/A')}") + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + sys.exit(1) + + if sys.argv[1] == "-": + queries = [line.strip() for line in sys.stdin if line.strip()] + else: + queries = sys.argv[1:] + + for query in queries: + print(f"\n--- {query.upper()} (per 100g) ---") + data = search(query, max_results=2) + if not data or not data.get("foods"): + print(" No results found.") + else: + for food in data["foods"]: + display(food) + print() + if len(queries) > 1: + time.sleep(1) # respect rate limits + + if API_KEY == "DEMO_KEY": + print("\nTip: using DEMO_KEY (30 req/hr). Set USDA_API_KEY for 1000 req/hr.") + print("Free signup: https://fdc.nal.usda.gov/api-key-signup/") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/skills_tool.py b/tools/skills_tool.py index 5a9e80f34..90839b9a7 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -245,6 +245,9 @@ def _get_required_environment_variables( if isinstance(required_for, str) and required_for.strip(): normalized["required_for"] = required_for.strip() + if entry.get("optional"): + normalized["optional"] = True + seen.add(env_name) required.append(normalized) @@ -378,6 +381,8 @@ def _remaining_required_environment_names( remaining = [] for entry in required_env_vars: name = entry["name"] + if entry.get("optional"): + continue if name in missing_names or not _is_env_var_persisted(name, env_snapshot): remaining.append(name) return remaining @@ -1042,7 +1047,8 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str: missing_required_env_vars = [ e for e in required_env_vars - if not _is_env_var_persisted(e["name"], env_snapshot) + if not e.get("optional") + and not _is_env_var_persisted(e["name"], env_snapshot) ] capture_result = _capture_required_environment_variables( skill_name,