crono is a CLI for automating Cronometer (nutrition tracking) via Kernel.sh browser automation. Cronometer has no public API, so we automate the web UI.
docs/prds/00-overview.md— Project goals, tech stack, how it worksdocs/prds/02-command-quick-add.md— Detailed spec for the quick-add command
src/
├── index.ts # CLI entry (Commander.js)
├── commands/
│ ├── diary.ts # Daily nutrition totals command
│ ├── login.ts # Credential setup command
│ ├── quick-add.ts # Macro entry command
│ └── weight.ts # Weight reading command
├── kernel/
│ ├── client.ts # Kernel.sh SDK orchestration
│ ├── diary.ts # Playwright codegen for diary scraping
│ ├── login.ts # Playwright codegen for login
│ ├── quick-add.ts # Playwright codegen for quick-add
│ └── weight.ts # Playwright codegen for weight scraping
├── utils/
│ └── date.ts # Date validation and range parsing
├── config.ts # ~/.config/crono/ management
└── credentials.ts # Keychain + encrypted file credential storage
Kernel.sh is a browser automation platform. Key concepts:
- Browsers — Ephemeral browser sessions for automation
- Playwright — Remote code execution against browser pages
- SDK — TypeScript API for session and page interaction
Our flow:
CLI Command → Kernel Client → Kernel.sh SDK → Browser → Cronometer
Each command creates a fresh browser, logs in, performs the action, and tears down.
Implemented:
- Project scaffolding
- CLI structure with Commander.js
-
quick-addcommand (validation, argument parsing, full automation) -
logincommand (credential setup with keychain storage) - Kernel.sh SDK integration in
src/kernel/client.ts - Cronometer login flow (auto with stored creds, manual via live view)
- UI automation with Playwright codegen (smart waits, GWT-compatible)
- Credential storage (OS keychain primary, AES-256-GCM encrypted file fallback)
- Config management (
~/.config/crono/) - CI pipeline (GitHub Actions, Ubuntu, Node 18/20/22)
- @clack/prompts CLI UX (spinners, styled output, cancel handling)
-
weightcommand (date/range support, JSON output) -
diarycommand (daily nutrition totals, date/range support, JSON output) - Date utilities (
src/utils/date.ts) shared across commands (includesresolveDatefor quick-add) -
quick-add --dateflag (retroactive logging via prev-day arrow navigation) -
quick-add --alcoholflag (alcohol grams as separate macro) - npm publishing (
@milldr/crono) with trusted publishing via OIDC - Release Drafter + automated publish workflow
- Branch protection (PRs required against main)
TODO:
- Session persistence via Kernel profiles (requires paid plan)
- Additional commands (
search,add,summary,export)
crono quick-add -p 30 -c 100 -f 20 -a 14 -m Dinner -d yesterdayFlags:
-p, --protein <g>— Grams of protein-c, --carbs <g>— Grams of carbs-f, --fat <g>— Grams of fat-a, --alcohol <g>— Grams of alcohol-m, --meal <name>— Breakfast, Lunch, Dinner, Snacks (default: Uncategorized)-d, --date <date>— Target date (YYYY-MM-DD, yesterday, -Nd; default: today)
Validation: At least one macro required (-p, -c, -f, or -a).
Each macro (protein, carbs, fat, alcohol) is added as a separate "Quick Add" food item:
- Navigate to
cronometer.com/#diary - If
--dateis set, click prev-day arrow to reach the target date (2s wait per click for GWT re-render) - Right-click the meal category (e.g. "Dinner")
- Click "Add Food..." in context menu
- Search for the macro (e.g. "Quick Add, Protein")
- Click SEARCH, select the result
- Enter serving size in grams
- Click "ADD TO DIARY"
- Repeat for each macro
Date navigation note: Cronometer's GWT hash routing does not support ?date= query params. Date changes use prev-day arrow clicks (same approach as diary and weight commands), capped at 90 days back.
Uses event-driven Playwright waits (networkidle, waitForSelector) with a 2s stabilization wait after initial navigation for GWT rendering. GWT-compatible input handling via native setter + event dispatch.
~/.config/crono/
└── config.json # User settings
npm install # Install deps
npm run dev -- <cmd> # Run without building
npm run build # Compile TypeScript
npm test # Run tests
npm run lint # ESLint
npm run format # PrettierImportant: The globally installed crono command runs from dist/, not src/. After changing source files, always run npm run build before testing with crono. Use npm run dev -- <cmd> to skip the build step during development.
- Create
src/commands/<name>.ts - Export the handler function
- Register in
src/index.tswith Commander
Package is published to npm as @milldr/crono.
Workflow:
- PRs merged to
main→ Release Drafter auto-updates a draft GitHub Release - Release Drafter resolves the next version from PR labels (
minordefault,major/patchvia labels) - Review the draft at GitHub Releases, edit if needed
- Click "Publish release" →
.github/workflows/release.ymlruns CI and publishes to npm - The publish workflow extracts the version from the release tag and sets it in
package.jsonbefore publishing — no manual version bump needed
Version Bumping:
- PR labels control the version bump:
major,minor/feature/enhancement,patch/fix/bugfix/bug - Default bump is
minor(when no version label is present) no releaselabel signals a PR doesn't warrant a release (just don't publish the draft)- The git tag is the source of truth for the version, not
package.json
npm Authentication:
- Uses OIDC trusted publishing — no
NPM_TOKENsecret needed - Requires npm >= 11.5 for OIDC auth (the publish workflow upgrades npm)
setup-nodemust NOT useregistry-url(it injectsGITHUB_TOKENasNODE_AUTH_TOKEN, overriding OIDC)package.jsonmust have arepository.urlmatching the GitHub repo (required for provenance validation)
Other Details:
- The publish workflow runs format check, lint, build, and tests before publishing
--provenanceflag is included for npm supply chain securityfilesfield inpackage.jsonlimits the published tarball todist/only- Branch protection requires PRs against
main(admins can bypass)
tests/— Unit tests (Vitest)- Integration tests require Kernel.sh account + Cronometer login
- Run:
npm testornpm run test:watch