huckleberry
Track baby sleep, feeding, diapers, and growth via Huckleberry app API. Use for logging baby activities through natural language.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-openclaw-huckleberry-skill
Repository
Skill path: skills/aaronn/openclaw-huckleberry-skill
Track baby sleep, feeding, diapers, and growth via Huckleberry app API. Use for logging baby activities through natural language.
Open repositoryBest for
Primary workflow: Grow & Distribute.
Technical facets: Full Stack, Backend.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install huckleberry into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding huckleberry to shared team environments
- Use huckleberry for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: huckleberry
description: Track baby sleep, feeding, diapers, and growth via Huckleberry app API. Use for logging baby activities through natural language.
homepage: https://github.com/aaronn/openclaw-huckleberry-skill
metadata:
clawdbot:
emoji: "👶"
requires:
bins: ["python3"]
packages: ["huckleberry-api"]
install:
- id: pip-huckleberry
kind: pip
package: huckleberry-api
label: Install huckleberry-api (pip)
---
# Huckleberry Baby Tracker
Track baby activities (sleep, feeding, diapers, growth) via the Huckleberry app's Firebase backend.
## Setup
1. Install the API:
```bash
# Install from GitHub (required for bottle feeding support until next PyPI release)
pip install git+https://github.com/Woyken/py-huckleberry-api.git
# or with uv:
uv pip install git+https://github.com/Woyken/py-huckleberry-api.git
```
2. Configure credentials (choose one):
- Environment variables:
```bash
export HUCKLEBERRY_EMAIL="[email protected]"
export HUCKLEBERRY_PASSWORD="your-password"
export HUCKLEBERRY_TIMEZONE="America/Los_Angeles" # optional
```
- Config file at `~/.config/huckleberry/credentials.json`:
```json
{
"email": "[email protected]",
"password": "your-password",
"timezone": "America/Los_Angeles"
}
```
## CLI Usage
The CLI is at `~/clawd/skills/huckleberry/scripts/hb.py`
```bash
# List children
python3 ~/clawd/skills/huckleberry/scripts/hb.py children
# Sleep tracking
python3 ~/clawd/skills/huckleberry/scripts/hb.py sleep-start
python3 ~/clawd/skills/huckleberry/scripts/hb.py sleep-pause
python3 ~/clawd/skills/huckleberry/scripts/hb.py sleep-resume
python3 ~/clawd/skills/huckleberry/scripts/hb.py sleep-complete
python3 ~/clawd/skills/huckleberry/scripts/hb.py sleep-cancel
# Breastfeeding
python3 ~/clawd/skills/huckleberry/scripts/hb.py feed-start --side left
python3 ~/clawd/skills/huckleberry/scripts/hb.py feed-switch
python3 ~/clawd/skills/huckleberry/scripts/hb.py feed-pause
python3 ~/clawd/skills/huckleberry/scripts/hb.py feed-resume --side right
python3 ~/clawd/skills/huckleberry/scripts/hb.py feed-complete
python3 ~/clawd/skills/huckleberry/scripts/hb.py feed-cancel
# Bottle feeding
python3 ~/clawd/skills/huckleberry/scripts/hb.py bottle 120 --type "Formula" --units ml
# Diaper
python3 ~/clawd/skills/huckleberry/scripts/hb.py diaper both --pee-amount medium --poo-amount big --color yellow --consistency loose
# Growth
python3 ~/clawd/skills/huckleberry/scripts/hb.py growth --weight 5.2 --height 55 --head 38 --units metric
python3 ~/clawd/skills/huckleberry/scripts/hb.py growth-get
# History
python3 ~/clawd/skills/huckleberry/scripts/hb.py history --date 2026-01-27
python3 ~/clawd/skills/huckleberry/scripts/hb.py history --days 7 --type sleep --type feed
python3 ~/clawd/skills/huckleberry/scripts/hb.py history --json
```
## Complete Parameter Reference
### Sleep Commands
| Command | Parameters | Description |
|---------|------------|-------------|
| `sleep-start` | — | Start a new sleep session (timer begins) |
| `sleep-pause` | — | Pause the current sleep session |
| `sleep-resume` | — | Resume a paused sleep session |
| `sleep-complete` | `--notes` | End sleep and save to history |
| `sleep-cancel` | — | Cancel without saving to history |
### Breastfeeding Commands
| Command | Parameters | Description |
|---------|-----------|-------------|
| `feed-start` | `--side {left,right}` (default: left) | Start nursing session |
| `feed-pause` | — | Pause session, accumulate duration |
| `feed-resume` | `--side {left,right}` (optional) | Resume on specified or last side |
| `feed-switch` | — | Switch to other side (auto-resumes if paused) |
| `feed-complete` | `--notes` | End session and save to history |
| `feed-cancel` | — | Cancel without saving |
### Bottle Feeding
```
bottle <amount> [options]
```
| Parameter | Values | Required | Default |
|-----------|--------|----------|---------|
| `amount` | Any number | **Yes** | — |
| `--type` / `-t` | `"Breast Milk"`, `"Formula"`, `"Mixed"` | No | `"Formula"` |
| `--units` / `-u` | `ml`, `oz` | No | `ml` |
| `--notes` / `-n` | Any text | No | — |
### Diaper Change
```
diaper <mode> [options]
```
| Parameter | Values | Required | Default |
|-----------|--------|----------|---------|
| `mode` | `pee`, `poo`, `both`, `dry` | **Yes** | — |
| `--pee-amount` | `little`, `medium`, `big` | No | — |
| `--poo-amount` | `little`, `medium`, `big` | No | — |
| `--color` | `yellow`, `brown`, `black`, `green`, `red`, `gray` | No | — |
| `--consistency` | `solid`, `loose`, `runny`, `mucousy`, `hard`, `pebbles`, `diarrhea` | No | — |
| `--rash` | (flag) | No | false |
| `--notes` | Any text | No | — |
#### Color Guide
- **yellow** — Normal for breastfed babies
- **brown** — Normal for formula-fed or older babies
- **green** — Can be normal, or indicate fast digestion/foremilk
- **black** — Normal first few days (meconium), concerning later
- **red** — May indicate blood, consult pediatrician
- **gray** — Uncommon, may indicate liver issues
#### Consistency Guide
- **solid** — Formed stool
- **loose** — Soft but not watery
- **runny** — Watery consistency
- **mucousy** — Contains mucus
- **hard** — Firm, may indicate constipation
- **pebbles** — Small hard pieces
- **diarrhea** — Very watery, frequent
### Growth Measurements
```
growth [options]
growth-get
```
| Parameter | Values | Required | Notes |
|-----------|--------|----------|-------|
| `--weight` / `-w` | Number | At least one | kg (metric) or lbs (imperial) |
| `--height` / `-l` | Number | measurement | cm (metric) or inches (imperial) |
| `--head` | Number | required | cm (metric) or inches (imperial) |
| `--units` / `-u` | `metric`, `imperial` | No | Default: `metric` |
| `--notes` / `-n` | Any text | No | — |
### History / Calendar
```
history [options]
```
| Parameter | Values | Required | Default |
|-----------|--------|----------|---------|
| `--date` / `-d` | `YYYY-MM-DD` | No | Today |
| `--days` | Number | No | 1 |
| `--type` / `-t` | `sleep`, `feed`, `diaper`, `health` | No | All types |
Use `--type` multiple times to filter: `--type sleep --type feed`
## Agent Guidelines: When to Ask for Details
### AI Attribution on Notes
**Always** include AI attribution when logging entries:
**Creating new entries:**
- No user note: `--notes "Created via AI"`
- User provides note: `--notes "user's note | Created via AI"`
**Editing existing entries:**
- No user note: `--notes "Updated via AI"`
- User provides note: append ` | Updated via AI` to existing notes
This creates a paper trail for AI-assisted entries.
### When to Ask for Clarification
When a user request is sparse, ask for clarification before logging. Here's when:
### Diaper Changes
If user says just "diaper change" or "poop":
- **Always ask:** Was it pee, poo, or both?
- **For poo, consider asking:** Color? Consistency? Amount?
- **Skip details if:** User seems rushed or says "just log it"
Example follow-up:
> "Got it! Was it pee, poo, or both? Any details to note (color, consistency, amount)?"
### Bottle Feeding
If user says "bottle" without amount:
- **Always ask:** How much? (in ml or oz)
- **Consider asking:** Formula, breast milk, or mixed?
Example follow-up:
> "How much was the bottle? And was it formula, breast milk, or mixed?"
### Growth Measurements
If user says "log weight" without value:
- **Always ask:** What's the weight? (and clarify units if ambiguous)
### Sleep/Feeding Timers
These are typically clear commands, but clarify if ambiguous:
- "Baby's eating" → "Starting breastfeeding — which side, left or right?"
- "Feed done" → Check if breastfeeding or bottle context
## Natural Language Examples
| User says | Action |
|-----------|--------|
| "Baby fell asleep" | `sleep-start` |
| "She woke up" | `sleep-complete` |
| "Cancel that sleep" | `sleep-cancel` |
| "Feeding on the left" | `feed-start --side left` |
| "Switch sides" | `feed-switch` |
| "Done nursing" | `feed-complete` |
| "4oz bottle of formula" | `bottle 4 --type Formula --units oz` |
| "120ml breast milk bottle" | `bottle 120 --type "Breast Milk" --units ml` |
| "Diaper change, pee and poo" | → Ask about amounts/color/consistency |
| "Just a wet diaper" | `diaper pee` |
| "Dry check" | `diaper dry` |
| "Weight is 5.5kg" | `growth --weight 5.5 --units metric` |
| "What did the baby do today?" | `history --days 1` |
| "Sleep history for the week" | `history --days 7 --type sleep` |
## Multi-Child Support
If the account has multiple children, use `--child` / `-c`:
```bash
python3 hb.py --child "Baby Name" sleep-start
```
Without `--child`, commands default to the first child in the account.
## Troubleshooting
**Authentication errors:**
- Verify email/password are correct
- Check credentials file permissions
- Huckleberry doesn't support 2FA for API access
**"No children found":**
- Ensure the account has at least one child profile in the Huckleberry app
**Timer already active:**
- Complete or cancel the existing session before starting a new one
## Technical Notes
- Uses Firebase Firestore via gRPC (same as mobile app)
- Real-time sync: Changes appear immediately in the Huckleberry app
- Token auto-refresh: Sessions stay authenticated
- **Timezone handling:** Huckleberry requires an `offset` field (minutes behind UTC) for entries to display correctly. E.g., PST (UTC-8) = 480 minutes. The CLI automatically calculates this from the configured timezone. Without this field, entries appear at UTC time in the app.
---
## Credits
Built on [py-huckleberry-api](https://github.com/Woyken/py-huckleberry-api) by Woyken — a reverse-engineered Python client for Huckleberry's Firebase backend.
---
*Created with AI - 2026-01-27*
*Updated with AI - 2026-01-28*
## Notes on All Entry Types
The `--notes` / `-n` parameter is available on all entry types:
- `sleep-complete --notes "Slept through the night!"`
- `feed-complete --notes "Good latch today"`
- `bottle --notes "Logged via AI"`
- `diaper --notes "Checked by AI"`
- `growth --notes "Measured at pediatrician"`
The upstream py-huckleberry-api only supports notes on diaper entries. This skill extends that to all types by updating the Firestore document directly after creation.
## Not Supported by Upstream API
The following features exist in Huckleberry but aren't exposed in the py-huckleberry-api:
- Sleep conditions (happy/upset at start/end)
- Sleep locations (car, nursing, stroller, crib, etc.)
These would require modifying the upstream library to accept additional parameters.
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "aaronn",
"slug": "openclaw-huckleberry-skill",
"displayName": "Huckleberry",
"latest": {
"version": "0.1.0",
"publishedAt": 1770233485058,
"commit": "https://github.com/clawdbot/skills/commit/5b872523912e20493fa2d49ffbcecba9fd0cb8b0"
},
"history": []
}
```
### scripts/hb.py
```python
#!/usr/bin/env python3
"""
Huckleberry Baby Tracker CLI
A command-line interface for the Huckleberry baby tracking app.
Requires: pip install huckleberry-api
Auth: Set HUCKLEBERRY_EMAIL and HUCKLEBERRY_PASSWORD environment variables,
or use a credentials file at ~/.config/huckleberry/credentials.json
Created with AI - 2026-01-27
"""
import argparse
import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
try:
from huckleberry_api import HuckleberryAPI
from google.cloud import firestore
except ImportError:
print("Error: huckleberry-api not installed. Run: pip install huckleberry-api", file=sys.stderr)
sys.exit(1)
def get_credentials() -> tuple[str, str, str]:
"""Get credentials from environment or config file."""
email = os.environ.get("HUCKLEBERRY_EMAIL")
password = os.environ.get("HUCKLEBERRY_PASSWORD")
timezone = os.environ.get("HUCKLEBERRY_TIMEZONE", "America/Los_Angeles")
if not email or not password:
config_path = Path.home() / ".config" / "huckleberry" / "credentials.json"
if config_path.exists():
with open(config_path) as f:
creds = json.load(f)
email = email or creds.get("email")
password = password or creds.get("password")
timezone = creds.get("timezone", timezone)
if not email or not password:
print("Error: Missing credentials. Set HUCKLEBERRY_EMAIL and HUCKLEBERRY_PASSWORD", file=sys.stderr)
print("Or create ~/.config/huckleberry/credentials.json with {\"email\": ..., \"password\": ...}", file=sys.stderr)
sys.exit(1)
return email, password, timezone
def get_timezone_offset_minutes(tz_name: str) -> float:
"""Get timezone offset in minutes from UTC (positive = behind UTC, e.g. PST=480).
Huckleberry expects offset as positive minutes behind UTC.
E.g., PST (UTC-8) = 480 minutes.
"""
try:
import zoneinfo
tz = zoneinfo.ZoneInfo(tz_name)
# Get current offset
now = datetime.now(tz)
offset_seconds = now.utcoffset().total_seconds()
# Huckleberry uses positive values for behind UTC (opposite of standard)
return -offset_seconds / 60
except Exception:
# Fallback: assume PST
return 480.0
def get_api() -> HuckleberryAPI:
"""Initialize and authenticate API client."""
email, password, timezone = get_credentials()
api = HuckleberryAPI(email=email, password=password, timezone=timezone)
api.authenticate()
return api
def get_child_uid(api: HuckleberryAPI, child_name: str | None = None) -> str:
"""Get child UID, optionally filtering by name."""
children = api.get_children()
if not children:
print("Error: No children found in account", file=sys.stderr)
sys.exit(1)
if child_name:
for child in children:
if child["name"].lower() == child_name.lower():
return child["uid"]
print(f"Error: Child '{child_name}' not found. Available: {[c['name'] for c in children]}", file=sys.stderr)
sys.exit(1)
# Default to first child
return children[0]["uid"]
def add_notes_to_latest_interval(api: HuckleberryAPI, collection: str, child_uid: str, notes: str, subcollection: str = "intervals") -> None:
"""Add notes to the most recent interval document."""
client = api._get_firestore_client()
intervals_ref = client.collection(collection).document(child_uid).collection(subcollection)
# Get most recent interval
recent = list(intervals_ref.order_by("start", direction=firestore.Query.DESCENDING).limit(1).get())
if recent:
recent[0].reference.update({"notes": notes})
def cmd_children(args: argparse.Namespace) -> None:
"""List children in account."""
api = get_api()
children = api.get_children()
if args.json:
print(json.dumps(children, indent=2, default=str))
else:
for child in children:
print(f"- {child['name']} (uid: {child['uid']})")
if child.get("birthday"):
print(f" Birthday: {child['birthday']}")
if child.get("gender"):
print(f" Gender: {child['gender']}")
def cmd_sleep_log(args: argparse.Namespace) -> None:
"""Log a completed sleep with specific start time and duration."""
api = get_api()
child_uid = get_child_uid(api, args.child)
# Parse start time or calculate from end time
now = datetime.now()
if args.start:
# Parse start time (HH:MM or YYYY-MM-DD HH:MM)
try:
if len(args.start) <= 5: # HH:MM format, assume today
start = datetime.strptime(args.start, "%H:%M").replace(
year=now.year, month=now.month, day=now.day
)
else:
start = datetime.strptime(args.start, "%Y-%m-%d %H:%M")
except ValueError:
print("Error: Invalid start time format. Use HH:MM or YYYY-MM-DD HH:MM", file=sys.stderr)
sys.exit(1)
elif args.duration:
# Calculate start from duration (assume ended now or at specified end time)
if args.end:
try:
if len(args.end) <= 5:
end = datetime.strptime(args.end, "%H:%M").replace(
year=now.year, month=now.month, day=now.day
)
else:
end = datetime.strptime(args.end, "%Y-%m-%d %H:%M")
except ValueError:
print("Error: Invalid end time format. Use HH:MM or YYYY-MM-DD HH:MM", file=sys.stderr)
sys.exit(1)
else:
end = now
start = end - timedelta(minutes=args.duration)
else:
print("Error: Must specify --start or --duration", file=sys.stderr)
sys.exit(1)
# Calculate end time
if args.duration:
end = start + timedelta(minutes=args.duration)
elif args.end:
try:
if len(args.end) <= 5:
end = datetime.strptime(args.end, "%H:%M").replace(
year=now.year, month=now.month, day=now.day
)
else:
end = datetime.strptime(args.end, "%Y-%m-%d %H:%M")
except ValueError:
print("Error: Invalid end time format", file=sys.stderr)
sys.exit(1)
else:
end = now
duration_secs = int((end - start).total_seconds())
# Get timezone offset for Huckleberry (requires offset field for correct display)
_, _, tz_name = get_credentials()
offset_minutes = get_timezone_offset_minutes(tz_name)
# Write directly to Firestore
client = api._get_firestore_client()
intervals_ref = client.collection("sleep").document(child_uid).collection("intervals")
doc_data = {
"start": float(start.timestamp()),
"end": float(end.timestamp()),
"duration": float(duration_secs),
"offset": offset_minutes,
"end_offset": offset_minutes,
"pauses": [],
"lastUpdated": datetime.now().timestamp(),
}
if args.notes:
doc_data["notes"] = args.notes
intervals_ref.add(doc_data)
mins = duration_secs // 60
print(f"✓ Sleep logged: {start.strftime('%H:%M')} - {end.strftime('%H:%M')} ({mins}min)" +
(f" (notes: {args.notes})" if args.notes else ""))
def cmd_sleep_start(args: argparse.Namespace) -> None:
"""Start sleep tracking."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.start_sleep(child_uid)
print("✓ Sleep started")
def cmd_sleep_pause(args: argparse.Namespace) -> None:
"""Pause sleep tracking."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.pause_sleep(child_uid)
print("✓ Sleep paused")
def cmd_sleep_resume(args: argparse.Namespace) -> None:
"""Resume sleep tracking."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.resume_sleep(child_uid)
print("✓ Sleep resumed")
def cmd_sleep_cancel(args: argparse.Namespace) -> None:
"""Cancel sleep session without saving."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.cancel_sleep(child_uid)
print("✓ Sleep cancelled (not saved)")
def cmd_sleep_complete(args: argparse.Namespace) -> None:
"""Complete and save sleep session."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.complete_sleep(child_uid)
# Add notes if provided
if args.notes:
add_notes_to_latest_interval(api, "sleep", child_uid, args.notes)
print("✓ Sleep completed and saved" + (f" (notes: {args.notes})" if args.notes else ""))
def cmd_feed_start(args: argparse.Namespace) -> None:
"""Start breastfeeding session."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.start_feeding(child_uid, side=args.side)
print(f"✓ Feeding started on {args.side} side")
def cmd_feed_pause(args: argparse.Namespace) -> None:
"""Pause feeding session."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.pause_feeding(child_uid)
print("✓ Feeding paused")
def cmd_feed_resume(args: argparse.Namespace) -> None:
"""Resume feeding session."""
api = get_api()
child_uid = get_child_uid(api, args.child)
side = args.side if args.side else None
api.resume_feeding(child_uid, side=side)
print(f"✓ Feeding resumed" + (f" on {side} side" if side else ""))
def cmd_feed_switch(args: argparse.Namespace) -> None:
"""Switch feeding side."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.switch_feeding_side(child_uid)
print("✓ Switched feeding side")
def cmd_feed_cancel(args: argparse.Namespace) -> None:
"""Cancel feeding session without saving."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.cancel_feeding(child_uid)
print("✓ Feeding cancelled (not saved)")
def cmd_feed_complete(args: argparse.Namespace) -> None:
"""Complete and save feeding session."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.complete_feeding(child_uid)
# Add notes if provided
if args.notes:
add_notes_to_latest_interval(api, "feed", child_uid, args.notes)
print("✓ Feeding completed and saved" + (f" (notes: {args.notes})" if args.notes else ""))
def cmd_bottle(args: argparse.Namespace) -> None:
"""Log bottle feeding."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.log_bottle_feeding(
child_uid,
amount=args.amount,
bottle_type=args.type,
units=args.units
)
# Add notes if provided
if args.notes:
add_notes_to_latest_interval(api, "feed", child_uid, args.notes)
print(f"✓ Bottle feeding logged: {args.amount}{args.units} of {args.type}" + (f" (notes: {args.notes})" if args.notes else ""))
def cmd_diaper(args: argparse.Namespace) -> None:
"""Log diaper change."""
api = get_api()
child_uid = get_child_uid(api, args.child)
api.log_diaper(
child_uid,
mode=args.mode,
pee_amount=args.pee_amount,
poo_amount=args.poo_amount,
color=args.color,
consistency=args.consistency,
diaper_rash=args.rash,
notes=args.notes # Diaper already supports notes in upstream API
)
details = [args.mode]
if args.pee_amount:
details.append(f"pee={args.pee_amount}")
if args.poo_amount:
details.append(f"poo={args.poo_amount}")
if args.color:
details.append(f"color={args.color}")
if args.consistency:
details.append(f"consistency={args.consistency}")
if args.rash:
details.append("diaper rash")
if args.notes:
details.append(f"notes: {args.notes}")
print(f"✓ Diaper logged: {', '.join(details)}")
def cmd_growth(args: argparse.Namespace) -> None:
"""Log growth measurements."""
api = get_api()
child_uid = get_child_uid(api, args.child)
if not args.weight and not args.height and not args.head:
print("Error: At least one measurement required (--weight, --height, or --head)", file=sys.stderr)
sys.exit(1)
api.log_growth(
child_uid,
weight=args.weight,
height=args.height,
head=args.head,
units=args.units
)
# Add notes if provided (health uses "data" subcollection, not "intervals")
if args.notes:
add_notes_to_latest_interval(api, "health", child_uid, args.notes, subcollection="data")
measurements = []
unit_suffix = "kg/cm" if args.units == "metric" else "lbs/in"
if args.weight:
measurements.append(f"weight={args.weight}")
if args.height:
measurements.append(f"height={args.height}")
if args.head:
measurements.append(f"head={args.head}")
print(f"✓ Growth logged ({unit_suffix}): {', '.join(measurements)}" + (f" (notes: {args.notes})" if args.notes else ""))
def cmd_growth_get(args: argparse.Namespace) -> None:
"""Get latest growth measurements."""
api = get_api()
child_uid = get_child_uid(api, args.child)
data = api.get_growth_data(child_uid)
if args.json:
print(json.dumps(data, indent=2, default=str))
else:
print("Latest growth measurements:")
if data.get("weight"):
print(f" Weight: {data['weight']} {data.get('weight_units', 'kg')}")
if data.get("height"):
print(f" Height: {data['height']} {data.get('height_units', 'cm')}")
if data.get("head"):
print(f" Head: {data['head']} {data.get('head_units', 'cm')}")
if data.get("timestamp_sec"):
dt = datetime.fromtimestamp(data["timestamp_sec"])
print(f" Recorded: {dt.strftime('%Y-%m-%d %H:%M')}")
def cmd_history(args: argparse.Namespace) -> None:
"""Get history of events for a date range."""
api = get_api()
child_uid = get_child_uid(api, args.child)
# Parse date range
if args.date:
# Single date
try:
date = datetime.strptime(args.date, "%Y-%m-%d")
except ValueError:
print(f"Error: Invalid date format. Use YYYY-MM-DD", file=sys.stderr)
sys.exit(1)
start = date.replace(hour=0, minute=0, second=0)
end = date.replace(hour=23, minute=59, second=59)
else:
# Default: today
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
days_back = args.days or 1
start = today - timedelta(days=days_back - 1)
end = datetime.now()
start_ts = int(start.timestamp())
end_ts = int(end.timestamp())
# Get events based on type filter
events = {}
event_types = args.type if args.type else ["sleep", "feed", "diaper", "health"]
if "sleep" in event_types:
events["sleep"] = api.get_sleep_intervals(child_uid, start_ts, end_ts)
if "feed" in event_types:
events["feed"] = api.get_feed_intervals(child_uid, start_ts, end_ts)
if "diaper" in event_types:
events["diaper"] = api.get_diaper_intervals(child_uid, start_ts, end_ts)
if "health" in event_types:
events["health"] = api.get_health_entries(child_uid, start_ts, end_ts)
if args.json:
print(json.dumps(events, indent=2, default=str))
else:
print(f"History from {start.strftime('%Y-%m-%d %H:%M')} to {end.strftime('%Y-%m-%d %H:%M')}:")
for event_type, items in events.items():
if items:
print(f"\n{event_type.upper()} ({len(items)} events):")
for item in items:
ts = datetime.fromtimestamp(item.get("start", 0))
time_str = ts.strftime("%Y-%m-%d %H:%M")
if event_type == "sleep":
duration = item.get("duration", 0)
mins = duration // 60
print(f" {time_str} - {mins}min")
elif event_type == "feed":
left = item.get("leftDuration", 0)
right = item.get("rightDuration", 0)
if left or right:
print(f" {time_str} - L:{left:.0f}s R:{right:.0f}s")
else:
print(f" {time_str}")
elif event_type == "diaper":
mode = item.get("mode", "?")
print(f" {time_str} - {mode}")
elif event_type == "health":
parts = []
if item.get("weight"):
parts.append(f"weight={item['weight']}")
if item.get("height"):
parts.append(f"height={item['height']}")
if item.get("head"):
parts.append(f"head={item['head']}")
print(f" {time_str} - {', '.join(parts) if parts else 'growth'}")
def main() -> None:
parser = argparse.ArgumentParser(
description="Huckleberry Baby Tracker CLI",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("--child", "-c", help="Child name (defaults to first child)")
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Children
subparsers.add_parser("children", help="List children in account")
# Sleep commands
sleep_log = subparsers.add_parser("sleep-log", help="Log a completed sleep with specific times")
sleep_log.add_argument("--start", "-s", help="Start time (HH:MM or YYYY-MM-DD HH:MM)")
sleep_log.add_argument("--end", "-e", help="End time (HH:MM or YYYY-MM-DD HH:MM, default: now)")
sleep_log.add_argument("--duration", "-d", type=int, help="Duration in minutes")
sleep_log.add_argument("--notes", "-n", help="Notes for this sleep")
subparsers.add_parser("sleep-start", help="Start sleep tracking")
subparsers.add_parser("sleep-pause", help="Pause sleep tracking")
subparsers.add_parser("sleep-resume", help="Resume sleep tracking")
subparsers.add_parser("sleep-cancel", help="Cancel sleep (don't save)")
sleep_complete = subparsers.add_parser("sleep-complete", help="Complete and save sleep")
sleep_complete.add_argument("--notes", "-n", help="Notes for this sleep session")
# Feed commands
feed_start = subparsers.add_parser("feed-start", help="Start breastfeeding")
feed_start.add_argument("--side", "-s", choices=["left", "right"], default="left",
help="Starting side (default: left)")
subparsers.add_parser("feed-pause", help="Pause feeding")
feed_resume = subparsers.add_parser("feed-resume", help="Resume feeding")
feed_resume.add_argument("--side", "-s", choices=["left", "right"],
help="Side to resume on (optional)")
subparsers.add_parser("feed-switch", help="Switch feeding side")
subparsers.add_parser("feed-cancel", help="Cancel feeding (don't save)")
feed_complete = subparsers.add_parser("feed-complete", help="Complete and save feeding")
feed_complete.add_argument("--notes", "-n", help="Notes for this feeding session")
# Bottle command
bottle = subparsers.add_parser("bottle", help="Log bottle feeding")
bottle.add_argument("amount", type=float, help="Amount fed")
bottle.add_argument("--type", "-t", choices=["Breast Milk", "Formula", "Mixed"],
default="Formula", help="Bottle contents (default: Formula)")
bottle.add_argument("--units", "-u", choices=["ml", "oz"], default="ml",
help="Volume units (default: ml)")
bottle.add_argument("--notes", "-n", help="Notes for this bottle feeding")
# Diaper command
diaper = subparsers.add_parser("diaper", help="Log diaper change")
diaper.add_argument("mode", choices=["pee", "poo", "both", "dry"],
help="Diaper type")
diaper.add_argument("--pee-amount", choices=["little", "medium", "big"],
help="Pee amount")
diaper.add_argument("--poo-amount", choices=["little", "medium", "big"],
help="Poo amount")
diaper.add_argument("--color", choices=["yellow", "brown", "black", "green", "red", "gray"],
help="Poo color")
diaper.add_argument("--consistency",
choices=["solid", "loose", "runny", "mucousy", "hard", "pebbles", "diarrhea"],
help="Poo consistency")
diaper.add_argument("--rash", action="store_true", help="Diaper rash present")
diaper.add_argument("--notes", "-n", help="Notes for this diaper change")
# Growth commands
growth = subparsers.add_parser("growth", help="Log growth measurements")
growth.add_argument("--weight", "-w", type=float, help="Weight (kg or lbs)")
growth.add_argument("--height", "-l", type=float, help="Height/length (cm or in)")
growth.add_argument("--head", type=float, help="Head circumference (cm or in)")
growth.add_argument("--units", "-u", choices=["metric", "imperial"], default="metric",
help="Measurement units (default: metric)")
growth.add_argument("--notes", "-n", help="Notes for this growth measurement")
subparsers.add_parser("growth-get", help="Get latest growth measurements")
# History command
history = subparsers.add_parser("history", help="Get history of events")
history.add_argument("--date", "-d", help="Date to fetch (YYYY-MM-DD)")
history.add_argument("--days", type=int, help="Number of days back (default: 1)")
history.add_argument("--type", "-t", action="append",
choices=["sleep", "feed", "diaper", "health"],
help="Event types to fetch (can repeat, default: all)")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
commands = {
"children": cmd_children,
"sleep-log": cmd_sleep_log,
"sleep-start": cmd_sleep_start,
"sleep-pause": cmd_sleep_pause,
"sleep-resume": cmd_sleep_resume,
"sleep-cancel": cmd_sleep_cancel,
"sleep-complete": cmd_sleep_complete,
"feed-start": cmd_feed_start,
"feed-pause": cmd_feed_pause,
"feed-resume": cmd_feed_resume,
"feed-switch": cmd_feed_switch,
"feed-cancel": cmd_feed_cancel,
"feed-complete": cmd_feed_complete,
"bottle": cmd_bottle,
"diaper": cmd_diaper,
"growth": cmd_growth,
"growth-get": cmd_growth_get,
"history": cmd_history,
}
commands[args.command](args)
if __name__ == "__main__":
main()
```