Add food-tracking skill with food_items and food_log CLI scripts

This commit is contained in:
Connor Rhodes 2026-05-23 08:56:48 -05:00
parent e86b720497
commit 13053168a0
4 changed files with 440 additions and 0 deletions

View file

@ -47,6 +47,7 @@ description: Master index of all skills in your robot assistant system. Your ass
| "read this email," "review this email," "open this gmail," "check my gmail" | **gmail-browser** | | "read this email," "review this email," "open this gmail," "check my gmail" | **gmail-browser** |
| "make a query," "show all pages tagged," "build a filter," "list tasks in," "show recent notes," "create a silverbullet view," "make a live widget" | **silverbullet-query** | | "make a query," "show all pages tagged," "build a filter," "list tasks in," "show recent notes," "create a silverbullet view," "make a live widget" | **silverbullet-query** |
| "clipboard history," "from maccy," "what was on my clipboard," "get the last link I copied" | **clipboard-history** | | "clipboard history," "from maccy," "what was on my clipboard," "get the last link I copied" | **clipboard-history** |
| "log food," "add a food item," "what did I eat," "show my food log," "track macros," "food items," "daily food" | **food-tracking** |
--- ---
@ -250,6 +251,12 @@ description: Master index of all skills in your robot assistant system. Your ass
**File:** `skills/silverbullet-query/SKILL.md` **File:** `skills/silverbullet-query/SKILL.md`
**Dependencies:** SilverBullet running with Space Lua enabled **Dependencies:** SilverBullet running with Space Lua enabled
### Food Tracking
**Purpose:** Manage food items (catalog with nutrition info) and daily food log entries in MongoDB. Add new foods with macros, search the catalog, log meals and supplements, review eating history.
**Triggers:** "log food," "add a food item," "what did I eat," "show my food log," "track macros," "food items," "daily food"
**File:** `skills/food-tracking/SKILL.md`
**Dependencies:** Python with `pymongo`, `uv` CLI, scripts at `skills/food-tracking/scripts/`
### Clipboard History ### Clipboard History
**Purpose:** Retrieve items from clipboard history using Maccy on macOS. Query the Maccy SQLite database to find recently copied text, URLs, or search for specific content. macOS only. **Purpose:** Retrieve items from clipboard history using Maccy on macOS. Query the Maccy SQLite database to find recently copied text, URLs, or search for specific content. macOS only.
**Triggers:** "clipboard history," "from maccy," "what was on my clipboard," "get the last link I copied" **Triggers:** "clipboard history," "from maccy," "what was on my clipboard," "get the last link I copied"

100
food-tracking/SKILL.md Normal file
View file

@ -0,0 +1,100 @@
# Food Tracking
Manage food items and daily food log entries in MongoDB (`lists.food_items`, `lists.food_log`).
## Scripts
Both scripts are standalone Python CLIs. Run with `uv run --with pymongo`:
### food_items.py — `lists.food_items`
Reference item catalog: what foods exist, their nutrition, serving sizes, etc.
```bash
# Path relative to skill directory
SCRIPT="skills/food-tracking/scripts/food_items.py"
# Add a new food item
uv run --with pymongo "$SCRIPT" add \
--name "Kefir Oats" \
--type food \
--serving "1.5 cups" \
--description "Whole milk kefir with old-fashioned rolled oats" \
--nutrition '{"calories": 228, "protein_g": 12.1, "fat_g": 7.0, "carbs_g": 30.1, "fiber_g": 3.5, "sugar_g": 6.3}'
# Add a medicine
uv run --with pymongo "$SCRIPT" add \
--name "Excedrin Migraine" \
--type medicine \
--max-dosage "2 caplets"
# Add with arbitrary extra fields via JSON
uv run --with pymongo "$SCRIPT" add \
--name "Chicken Breast" \
--type food \
--serving "4 oz" \
--nutrition '{"calories": 187, "protein_g": 35, "fat_g": 4, "carbs_g": 0}' \
--extra '{"recipe_notes": "grilled, no oil"}'
# Get all items
uv run --with pymongo "$SCRIPT" get
# Filter by type
uv run --with pymongo "$SCRIPT" get --type food
# Search by name
uv run --with pymongo "$SCRIPT" get --name "kefir"
# Update an item by name
uv run --with pymongo "$SCRIPT" update --name "Kefir Oats" --set-serving "2 cups"
# Update an item by _id
uv run --with pymongo "$SCRIPT" update --id "6a11b0c3..." --set-nutrition '{"calories": 300, "protein_g": 15, "fat_g": 9, "carbs_g": 40}'
# Unset a field
uv run --with pymongo "$SCRIPT" update --name "Excedrin Migraine" --unset max_dosage
# Merge arbitrary fields
uv run --with pymongo "$SCRIPT" update --name "Chicken Breast" --set-extra '{"tags": ["dinner", "high-protein"]}'
```
### food_log.py — `lists.food_log`
Daily consumption log: what was eaten, when, and how much.
```bash
SCRIPT="skills/food-tracking/scripts/food_log.py"
# Log a food entry
uv run --with pymongo "$SCRIPT" add --name "Kefir Oats" --type food --amount 1 --unit "serving"
# Log a medicine
uv run --with pymongo "$SCRIPT" add --name "Excedrin Migraine" --type medicine --amount 1 --unit "caplet"
# Log with extra fields
uv run --with pymongo "$SCRIPT" add --name "Coffee" --type food --amount 2 --unit "cups" --extra '{"caffeine_mg": 200}'
# Get today's log
uv run --with pymongo "$SCRIPT" get --today
# Get last 7 days
uv run --with pymongo "$SCRIPT" get --days 7
# Filter by name or type
uv run --with pymongo "$SCRIPT" get --name "Excedrin"
uv run --with pymongo "$SCRIPT" get --type medicine
# Update an entry (requires --id from a previous get)
uv run --with pymongo "$SCRIPT" update --id "6a11a937..." --set-amount 2 --set-unit "caplets"
# Unset a field
uv run --with pymongo "$SCRIPT" update --id "6a11a937..." --unset unit
```
## Workflow Notes
- When logging food, the assistant should run `food_items.py get --name <item>` first to confirm the item exists and show its nutrition info.
- For new foods not in the catalog, add the item first, then log it.
- Always present nutrition info back to the user when logging food (pull from `food_items` so the user sees the macros).
- When searching the internet for nutrition facts, use USDA FoodData Central as the source, then store the result as a food item.
- The `--extra` flag on both scripts accepts arbitrary JSON, useful for fields not covered by named flags (e.g., recipe details, tags, notes).

View file

@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""CLI for managing food items in MongoDB lists.food_items."""
import argparse
import json
import sys
from datetime import datetime, timezone
from pymongo import MongoClient
CONN = "mongodb://root:3wwfoUjyk2E2zWELXFlLuHqfw1ALlOp4pb2H5Vq3TImbMIHL2h1u8Jej2mjzCPl@docdb.connorrhodes.com:35563/?tls=true&tlsAllowInvalidCertificates=true"
def get_collection():
client = MongoClient(CONN)
return client["lists"]["food_items"], client
def cmd_add(args):
coll, client = get_collection()
doc = {"name": args.name}
if args.type:
doc["type"] = args.type
if args.serving:
doc["serving"] = args.serving
if args.description:
doc["description"] = args.description
if args.max_dosage:
doc["max_dosage"] = args.max_dosage
if args.nutrition:
try:
doc["nutrition"] = json.loads(args.nutrition)
except json.JSONDecodeError:
print(f"Error: --nutrition must be valid JSON. Got: {args.nutrition}", file=sys.stderr)
sys.exit(1)
if args.extra:
try:
extra = json.loads(args.extra)
doc.update(extra)
except json.JSONDecodeError:
print(f"Error: --extra must be valid JSON. Got: {args.extra}", file=sys.stderr)
sys.exit(1)
result = coll.insert_one(doc)
out = coll.find_one({"_id": result.inserted_id})
out["_id"] = str(out["_id"])
print(json.dumps(out, indent=2, default=str))
client.close()
def cmd_get(args):
coll, client = get_collection()
query = {}
if args.name:
query["name"] = {"$regex": args.name, "$options": "i"}
if args.type:
query["type"] = {"$regex": args.type, "$options": "i"}
cursor = coll.find(query).sort("name", 1).limit(args.limit if args.limit else 50)
docs = []
for d in cursor:
d["_id"] = str(d["_id"])
docs.append(d)
if not docs:
print("No items found.")
else:
print(json.dumps(docs, indent=2, default=str))
client.close()
def cmd_update(args):
coll, client = get_collection()
# Build filter
if args.id:
from bson import ObjectId
query = {"_id": ObjectId(args.id)}
elif args.name:
query = {"name": args.name}
else:
print("Error: provide --id or --name to identify the item", file=sys.stderr)
sys.exit(1)
# Build update
update_fields = {}
if args.set_name:
update_fields["name"] = args.set_name
if args.set_type:
update_fields["type"] = args.set_type
if args.set_serving:
update_fields["serving"] = args.set_serving
if args.set_description:
update_fields["description"] = args.set_description
if args.set_max_dosage:
update_fields["max_dosage"] = args.set_max_dosage
if args.set_nutrition:
try:
update_fields["nutrition"] = json.loads(args.set_nutrition)
except json.JSONDecodeError:
print(f"Error: --set-nutrition must be valid JSON", file=sys.stderr)
sys.exit(1)
if args.set_extra:
try:
extra = json.loads(args.set_extra)
update_fields.update(extra)
except json.JSONDecodeError:
print(f"Error: --set-extra must be valid JSON", file=sys.stderr)
sys.exit(1)
if args.unset:
for field in args.unset:
update_fields[field] = ""
if not update_fields:
print("Error: nothing to update", file=sys.stderr)
sys.exit(1)
set_doc = {"$set": {k: v for k, v in update_fields.items()}}
unset_doc = {"$unset": {k: v for k, v in update_fields.items() if v == ""}}
update = {}
if set_doc["$set"]:
update["$set"] = set_doc["$set"]
if unset_doc["$unset"]:
update["$unset"] = unset_doc["$unset"]
result = coll.update_one(query, update)
print(f"Matched: {result.matched_count}, Modified: {result.modified_count}")
if result.matched_count:
updated = coll.find_one(query)
updated["_id"] = str(updated["_id"])
print(json.dumps(updated, indent=2, default=str))
else:
print("No matching item found.")
client.close()
def main():
parser = argparse.ArgumentParser(description="Manage food items in lists.food_items")
sub = parser.add_subparsers(dest="command", required=True)
# add
p_add = sub.add_parser("add", help="Add a new food item")
p_add.add_argument("--name", required=True)
p_add.add_argument("--type")
p_add.add_argument("--serving")
p_add.add_argument("--description")
p_add.add_argument("--max-dosage")
p_add.add_argument("--nutrition", help='JSON string, e.g. \'{"calories": 228, "protein_g": 12}\'')
p_add.add_argument("--extra", help="JSON string with additional fields to merge into the document")
# get
p_get = sub.add_parser("get", help="Get food items")
p_get.add_argument("--name", help="Filter by name (regex)")
p_get.add_argument("--type", help="Filter by type (regex)")
p_get.add_argument("--limit", type=int, help="Max items to return")
# update
p_upd = sub.add_parser("update", help="Update an existing food item")
p_upd.add_argument("--id", help="ObjectId to update")
p_upd.add_argument("--name", help="Name to match (if no --id)")
p_upd.add_argument("--set-name", dest="set_name")
p_upd.add_argument("--set-type", dest="set_type")
p_upd.add_argument("--set-serving", dest="set_serving")
p_upd.add_argument("--set-description", dest="set_description")
p_upd.add_argument("--set-max-dosage", dest="set_max_dosage")
p_upd.add_argument("--set-nutrition", dest="set_nutrition", help="JSON string to replace nutrition")
p_upd.add_argument("--set-extra", dest="set_extra", help="JSON string with additional fields to set")
p_upd.add_argument("--unset", nargs="*", help="Field names to remove")
args = parser.parse_args()
{"add": cmd_add, "get": cmd_get, "update": cmd_update}[args.command](args)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""CLI for managing food log entries in MongoDB lists.food_log."""
import argparse
import json
import sys
from datetime import datetime, timezone, timedelta
from pymongo import MongoClient
CONN = "mongodb://root:3wwfoUjyk2E2zWELXFlLuHqfw1ALlOp4pb2H5Vq3TImbMIHL2h1u8Jej2mjzCPl@docdb.connorrhodes.com:35563/?tls=true&tlsAllowInvalidCertificates=true"
def get_collection():
client = MongoClient(CONN)
return client["lists"]["food_log"], client
def cmd_add(args):
coll, client = get_collection()
doc = {"timestamp": datetime.now(timezone.utc)}
if args.name:
doc["name"] = args.name
if args.type:
doc["type"] = args.type
if args.amount:
doc["amount"] = args.amount
if args.unit:
doc["unit"] = args.unit
if args.extra:
try:
extra = json.loads(args.extra)
doc.update(extra)
except json.JSONDecodeError:
print(f"Error: --extra must be valid JSON. Got: {args.extra}", file=sys.stderr)
sys.exit(1)
if "name" not in doc:
print("Error: --name is required", file=sys.stderr)
sys.exit(1)
result = coll.insert_one(doc)
out = coll.find_one({"_id": result.inserted_id})
out["_id"] = str(out["_id"])
print(json.dumps(out, indent=2, default=str))
client.close()
def cmd_get(args):
coll, client = get_collection()
query = {}
if args.name:
query["name"] = {"$regex": args.name, "$options": "i"}
if args.type:
query["type"] = {"$regex": args.type, "$options": "i"}
if args.today:
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
query["timestamp"] = {"$gte": today_start}
if args.days:
cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
query["timestamp"] = {"$gte": cutoff}
cursor = coll.find(query).sort("timestamp", -1).limit(args.limit if args.limit else 50)
docs = []
for d in cursor:
d["_id"] = str(d["_id"])
docs.append(d)
if not docs:
print("No entries found.")
else:
print(json.dumps(docs, indent=2, default=str))
client.close()
def cmd_update(args):
coll, client = get_collection()
from bson import ObjectId
if not args.id:
print("Error: --id is required for update", file=sys.stderr)
sys.exit(1)
query = {"_id": ObjectId(args.id)}
update_fields = {}
if args.set_name:
update_fields["name"] = args.set_name
if args.set_type:
update_fields["type"] = args.set_type
if args.set_amount is not None:
update_fields["amount"] = args.set_amount
if args.set_unit:
update_fields["unit"] = args.set_unit
if args.set_extra:
try:
extra = json.loads(args.set_extra)
update_fields.update(extra)
except json.JSONDecodeError:
print(f"Error: --set-extra must be valid JSON", file=sys.stderr)
sys.exit(1)
if args.unset:
for field in args.unset:
update_fields[field] = ""
if not update_fields:
print("Error: nothing to update", file=sys.stderr)
sys.exit(1)
set_doc = {"$set": {k: v for k, v in update_fields.items()}}
unset_doc = {"$unset": {k: v for k, v in update_fields.items() if v == ""}}
update = {}
if set_doc["$set"]:
update["$set"] = set_doc["$set"]
if unset_doc["$unset"]:
update["$unset"] = unset_doc["$unset"]
result = coll.update_one(query, update)
print(f"Matched: {result.matched_count}, Modified: {result.modified_count}")
if result.matched_count:
updated = coll.find_one(query)
updated["_id"] = str(updated["_id"])
print(json.dumps(updated, indent=2, default=str))
else:
print("No matching entry found.")
client.close()
def main():
parser = argparse.ArgumentParser(description="Manage food log entries in lists.food_log")
sub = parser.add_subparsers(dest="command", required=True)
# add
p_add = sub.add_parser("add", help="Add a food log entry")
p_add.add_argument("--name", required=True)
p_add.add_argument("--type")
p_add.add_argument("--amount", type=float)
p_add.add_argument("--unit")
p_add.add_argument("--extra", help="JSON string with additional fields")
# get
p_get = sub.add_parser("get", help="Get food log entries")
p_get.add_argument("--name", help="Filter by name (regex)")
p_get.add_argument("--type", help="Filter by type (regex)")
p_get.add_argument("--today", action="store_true", help="Only today's entries")
p_get.add_argument("--days", type=int, help="Entries from last N days")
p_get.add_argument("--limit", type=int, help="Max entries to return")
# update
p_upd = sub.add_parser("update", help="Update a food log entry")
p_upd.add_argument("--id", required=True, help="ObjectId to update")
p_upd.add_argument("--set-name", dest="set_name")
p_upd.add_argument("--set-type", dest="set_type")
p_upd.add_argument("--set-amount", dest="set_amount", type=float)
p_upd.add_argument("--set-unit", dest="set_unit")
p_upd.add_argument("--set-extra", dest="set_extra", help="JSON string with additional fields to set")
p_upd.add_argument("--unset", nargs="*", help="Field names to remove")
args = parser.parse_args()
{"add": cmd_add, "get": cmd_get, "update": cmd_update}[args.command](args)
if __name__ == "__main__":
main()