# gfit-cli
A command-line client for the **GFIT wellness API** (`https://api.gfitwellness.ca`),
written in Rust. It mirrors every API call the `workout` desktop app makes —
**143 commands** covering auth, admin tools, coaching, nutrition, workouts,
check-ins, metrics (weight / step / sleep / text logs), invoices, and Nowledge.
Every command is self-documenting: run it with `-h` to print its full API
documentation (endpoint, auth requirement, and every parameter with type and
meaning).
## Install
Download the binary for your platform from the
[latest release](https://github.com/dickwu/gfit-cli/releases/latest) — assets are
named `gfit-cli-<target>` (e.g. `gfit-cli-aarch64-apple-darwin`):
```bash
# macOS (Apple Silicon) example
curl -fsSL -o gfit-cli https://github.com/dickwu/gfit-cli/releases/latest/download/gfit-cli-aarch64-apple-darwin
chmod +x gfit-cli
sudo mv gfit-cli /usr/local/bin/
gfit-cli --version
```
Available targets: `aarch64-apple-darwin`, `x86_64-apple-darwin`,
`x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`.
Once installed, keep it current with `gfit-cli self.update` (see [Updating](#updating)).
## Build from source
```bash
cargo build --release
# binary: target/release/gfit-cli
# (optionally) install it on your PATH:
install -m755 target/release/gfit-cli /usr/local/bin/gfit-cli
```
## Quick start
```bash
# 1. Log in — opens a browser sign-in page; the token is saved to ~/.config/gfit.json
gfit-cli auth.login
# …or log in directly without a browser (handy for scripts / headless boxes):
gfit-cli auth.login --email you@example.com --password 'secret'
# 2. Check who you are / where state lives
gfit-cli auth.status
# 3. Call any endpoint
gfit-cli coach.clients --search 'john'
gfit-cli admin.members --type all --page 1
gfit-cli coach.weight.log-list --uid 42 --start_date 2026-01-01
```
## Signing in
`gfit-cli auth.login` (with no `--email`/`--password`) opens a **browser sign-in
page** so your password never lands in shell history or the process list:
1. The CLI starts a tiny server bound to **`127.0.0.1` only**, on a random port,
and opens your browser to it (it also prints the URL in case the browser
doesn't open — e.g. open it yourself, or use the direct flow below on a
headless machine).
2. You enter your GFIT email + password in the page and submit.
3. The page posts the credentials back to the CLI, which exchanges them with the
GFIT API, saves the returned token to `~/.config/gfit.json`, and exits.
No external service or redirect is involved — everything stays on your machine.
The page must echo a one-time token the CLI generated, and the server shuts down
the moment login succeeds (or after 5 minutes).
Prefer to skip the browser (scripts, CI, SSH sessions)? Pass both credentials and
the CLI logs in directly, exactly as before:
```bash
gfit-cli auth.login --email you@example.com --password 'secret'
```
## Authentication is required
Every command talks to the API **only after you log in** (see
[Signing in](#signing-in)). With no saved token, the *only* things that run are
`auth.login`, the local `auth.status` / `whoami`, and `-h` / `--version`. Any
other command exits immediately:
```
$ gfit-cli coach.clients
error: not logged in — run `gfit-cli auth.login --email <e> --password <p>` first
```
The login check happens before argument validation, so you always get the
"not logged in" message rather than a confusing missing-parameter error.
`--dry-run` is exempt — it never calls the API, so you can inspect any request
offline (it prints `NOT logged in — this call would fail`).
## Command naming
Commands are **dot-named** as `group.action`, derived from the API path:
| API path prefix | Command group | Notes |
|-----------------|---------------|-------|
| `auth/…` | `auth.*` | login / logout / info |
| `admin/…` | `admin.*` | admin-tool surface |
| `staff/…` | `coach.*` | the app's "coach" views map to `staff/*` endpoints |
Examples:
```
auth/login -> auth.login
admin/user/list -> admin.users (alias: admin.members)
staff/workout/client/list -> coach.clients
staff/client/weight/log/list -> coach.weight.log-list
```
Run `gfit-cli` with no arguments to see the full, grouped command list. A few
ergonomic **aliases** exist (`admin.members`, `whoami`, …) and show up next to
their canonical command.
## Per-command API docs (`-h`)
```
$ gfit-cli auth.login -h
gfit-cli auth.login — Log in and save the token to ~/.config/gfit.json. With no --email/--password, opens a browser sign-in page; pass both to log in directly (scriptable)
Endpoint: POST auth/login
Auth: none
Parameters:
--email string [optional] Account email (omit to sign in via the browser)
--password string [optional] Account password (omit to sign in via the browser)
Common flags:
--json '{...}' Merge a raw JSON object into the request body
--dry-run Print the request instead of sending it
--raw Print the response as compact JSON
-h, --help Show this help
```
## Global flags (available on every command)
| Flag | Purpose |
|------|---------|
| `--dry-run` | Print the request (URL, auth, body) instead of sending it. Does **not** require a token. |
| `--json '{...}'` | Merge a raw JSON object into the request body (applied last, overrides other flags). |
| `--raw` | Print the response as compact JSON instead of pretty-printed. |
| `-h`, `--help` | Show the command's API documentation. |
| `-V`, `--version` | Print the version. |
### Passthrough parameters
Beyond documented parameters, **any** `--key value` flag is added to the JSON
request body. Values are auto-typed: `true`/`false` → bool, integers/decimals →
numbers, `{...}`/`[...]` → parsed JSON, everything else → string. This means
every endpoint is fully callable even where a field isn't formally documented:
```bash
gfit-cli admin.food.create --name Apple --calories 95 --carbs 25 \
--protein 0 --fat 0 --fibre 4 --weight 182 --measure 'medium' --category Fruit
```
## Updating
`gfit-cli` can upgrade itself from GitHub Releases — no package manager needed:
```bash
gfit-cli self.update --check # report current vs latest; install nothing
gfit-cli self.update # download + replace this binary if a newer one exists
gfit-cli self.update --force # reinstall the latest even if already current
gfit-cli self.update --insecure # skip SHA-256 checksum verification (not recommended)
```
It fetches the release asset matching the running platform, **verifies it against
the published `<asset>.sha256` checksum**, then swaps the binary in place
atomically (rolling back on failure). Downloads only follow redirects to
`https://*.github.com` hosts, and a mismatched or missing checksum aborts the
install (override with `--insecure`). If `gfit-cli` lives in a protected
directory (e.g. `/usr/local/bin`), run the update with `sudo`. This is a local
command — it talks to GitHub, not the GFIT API, so it needs no login.
## Skills
This repo ships a ready-to-install **Agent Skill** that teaches Claude to drive `gfit-cli`
well (see [`skills/`](skills/)). It uses the
[`npx skills`](https://github.com/vercel-labs/skills) flat layout, so it installs straight
into Claude:
```bash
npx skills add dickwu/gfit-cli --skill gfit-cli
```
The skill drives the `gfit-cli` binary, so [install it](#install) and run
`gfit-cli auth.login` ([Signing in](#signing-in)) first. Full step-by-step setup and usage
live in [`skills/README.md`](skills/).
| Skill | What it does |
|-------|--------------|
| [`gfit-cli`](skills/gfit-cli/SKILL.md) | Makes Claude fluent and safe with gfit-cli for any GFIT API operation — the command model, discovery (`gfit-cli` + `<cmd> -h`), auth, global flags, passthrough params, the `code == 1` / `data` envelope, exit codes, and a confirm-before-write workflow. Includes worked recipes (incl. a weekly client check-in review). |
### Use it in Claude Desktop (`.mcpb`)
For **Claude Desktop**, install the bundled MCP extension — it exposes gfit-cli as tools
Claude can call directly (Desktop can't run a CLI on its own).
**One-line install (macOS):** downloads the binary, fetches the latest extension and
registers it in `claude_desktop_config.json` (backing it up first), saves a usage guide,
and walks you through sign-in:
```bash
curl -fsSL https://raw.githubusercontent.com/dickwu/gfit-cli/main/install.sh | bash
```
Then fully quit and reopen Claude Desktop. The script checks for `curl` / `unzip` / `node`
and prints the exact Homebrew command if any is missing (installing Homebrew itself first
if needed); it needs Node to auto-register, and without it falls back to saving the `.mcpb`
and printing the **Settings → Extensions** import step. Want to read it first?
[`install.sh`](install.sh).
**Manual:** build from [`mcpb/`](mcpb/) (`npm install --omit=dev` then
`npx @anthropic-ai/mcpb pack .`) and drag `gfit-cli.mcpb` into **Settings → Extensions**, or
grab the prebuilt `.mcpb` from the
[MCPB release](https://github.com/dickwu/gfit-cli/releases?q=mcpb&expanded=true).
Either way you don't have to log in first — if you're signed out, the extension **opens the
browser sign-in for you** on first use (or call the `gfit_login` tool). Details:
[`mcpb/README.md`](mcpb/).
## App problem reports
Reports filed from the workout app's **Report** button (admin token required).
Timestamps are America/Winnipeg (the API's zone).
```bash
gfit-cli admin.app-report.list # pending, newest first (table)
gfit-cli admin.app-report.list --status all --limit 100
gfit-cli admin.app-report.list --since 24h --raw # compact JSON for scheduled jobs
gfit-cli admin.app-report.show 5 --tail 60 --download ~/Downloads/report-5
gfit-cli admin.app-report.pending-count
gfit-cli admin.app-report.set-status --id 5 --status dealing --dry-run
```
On `list`/`show`, `--json` (pretty) and `--raw` (compact) switch the **output** to JSON —
they don't merge into the request body. `list --json` prints
`{generated_at, timezone, filter, status_counts, count, has_more, reports[]}`; each report
carries the full `comment`, `tag`, `app_version`, `status`, `user_id`/`user_name`,
`has_screenshot`, `has_logs`, `attachment_count` and `created_at`. `--since` takes a
duration (`30m`, `24h`, `7d`, `2w`) or a date/time (`2026-10-26`, `'2026-10-26 09:00'`).
`show --json` decodes `info` and `logs` and includes every session-log entry.
## Configuration
State is stored at **`~/.config/gfit.json`**:
```json
{
"token": "…",
"email": "you@example.com"
}
```
Environment overrides:
| Variable | Effect |
|----------|--------|
| `GFIT_CONFIG` | Use a different config file path. |
| `GFIT_API_URL` | Override the API base URL (e.g. for staging). |
## Exit codes
| Code | Meaning |
|------|---------|
| `0` | Success (HTTP 2xx and API `code == 1`). |
| `1` | API or transport error (non-2xx, API `code != 1`, missing required arg, bad input). |
| `2` | Unknown command. |
## Project layout
```
build.rs bakes the build target triple in (GFIT_TARGET) for self-update asset matching
src/
main.rs entry point: dispatch, body building, help rendering, login routing
registry.rs single source of truth — every endpoint as a dot-named command + params
client.rs HTTP layer (reqwest blocking; JSON + multipart POST; GitHub get/download)
config.rs ~/.config/gfit.json load/save
args.rs --key value / --key=value / bare-flag parser
login.rs shared login helpers: exchange credentials for a token + persist it
weblogin.rs browser sign-in: localhost-only server that serves the login page
update.rs self-update: check GitHub Releases + replace the binary in place
app_report.rs admin.app-report.list/show: paging, --since, JSON output, downloads
tests/
cli.rs end-to-end tests driving the built binary (dry-run, never networks)
.github/workflows/
release.yml on tag push, build macOS/Linux binaries and attach them to the release
```
The registry is the only thing you edit to add or adjust endpoints; `main.rs`
drives entirely off it for dispatch, `-h` docs, body building, and auth.
## Tests
```bash
cargo test
```
Covers coercion, registry invariants (unique names, no alias collisions, auth
rules), and end-to-end CLI behavior via `--dry-run` (no network access).