From 14b678e5d2cb643126afa102c0af1f40f47ac4a9 Mon Sep 17 00:00:00 2001
From: DTTerastar
Date: Sat, 25 Apr 2026 18:17:45 -0400
Subject: [PATCH] feat: add 'prime' subcommand for LLM agent orientation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Per quantcli shared contract §6, every CLI exposes 'prime' printing a
one-screen primer aimed at LLM agents calling the CLI as a tool. Same
section structure across all three repos (WHAT IT IS / OUTPUT FORMATS /
AUTH / DATE FLAGS / SUBCOMMANDS / EXAMPLES / GOTCHAS) so an agent that
has read crono's prime knows where to look in liftoff's.
https://github.com/quantcli/common/blob/main/CONTRACT.md#6-the-prime-subcommand
Liftoff-specific content covers:
- workouts list/show/stats and bodyweights list/stats
- Local-zone date semantics
- LIFTOFF_API_BASE override for the periodic API host rotation
- jq recipes for volume math and bodyweight delta
Also extends the rootCmd Long description so 'liftoff-export --help'
points agents at 'prime' from the top.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
cmd/prime.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++
cmd/root.go | 6 +++
2 files changed, 137 insertions(+)
create mode 100644 cmd/prime.go
diff --git a/cmd/prime.go b/cmd/prime.go
new file mode 100644
index 0000000..acddb32
--- /dev/null
+++ b/cmd/prime.go
@@ -0,0 +1,131 @@
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+const primeText = `liftoff-export — primer for LLM agents
+======================================
+
+WHAT IT IS
+ A CLI that reads your personal Liftoff (gymbros.com) data — gym workouts
+ with sets/reps/weights, recorded bodyweights — and prints it on stdout.
+
+OUTPUT FORMATS
+ Default: narrow, fitdown-style markdown — date-grouped headings, one
+ exercise per block with set lines and Nx... compression for repeated
+ sets, easy to skim and easy for an LLM to consume inline.
+
+ --format json Pretty-printed JSON ARRAY of full posts/exercises.
+ Use this when you want the complete row, when piping
+ to jq, or when round-tripping into other tools.
+
+ Errors go to stderr. You do NOT need '2>&1'. Exit code is 0 on
+ success and non-zero on auth or network failure. An empty result is
+ success — markdown prints "No workouts found.", JSON prints '[]'.
+
+AUTH
+ 'liftoff-export auth login' opens an interactive prompt for email/
+ password and writes ~/.config/liftoff-export/auth.json (access token,
+ refresh token, expiry). Subsequent calls auto-refresh when the access
+ token is within 5 minutes of expiry.
+
+ 'liftoff-export auth status' is a fast local check that exits 0 when a
+ saved token is present and not yet expired, 1 with a clear "not logged
+ in" or "token expired" message otherwise. No network call.
+
+ 'liftoff-export auth refresh' forces a refresh now.
+ 'liftoff-export auth logout' deletes the stored tokens.
+
+ Liftoff retires version-pinned API hosts periodically. If a refresh
+ starts failing with "server is deprecated", set LIFTOFF_API_BASE=
+ https://vX-Y-Z.api.getgymbros.com to point at a current version
+ without waiting for a new release.
+
+DATE FLAGS (every export subcommand accepts these)
+ --since VALUE inclusive lower bound
+ --until VALUE inclusive upper bound; defaults to now
+ VALUE: today | yesterday | YYYY-MM-DD | Nd/Nw/Nm/Ny
+
+ See https://github.com/quantcli/common/blob/main/CONTRACT.md#3-date-flags
+ for the cross-CLI specification.
+
+SUBCOMMANDS
+
+ workouts list — every workout you've logged.
+ Markdown: 'Workout MONTH D, YYYY' headings; one exercise block per
+ movement with set lines. Bodyweight-relative sets render as
+ 'reps@-assist' (assisted) or 'reps@+added' (banded).
+ JSON: full Post array. Keys (subset):
+ id, startedAt, postedAt, sessionDuration, sessionNotes,
+ bodyweight, caloriesBurned, prCount,
+ exerciseData: [{ exerciseName, exerciseTypes, setsData: [...] }]
+
+ Filters: --exercise NAME (word-prefix match: 'bench' → 'Bench Press').
+
+ workouts show DATE
+ Same shape as 'list' but only workouts on DATE. DATE is the same
+ vocabulary as --since (today, yesterday, YYYY-MM-DD). Useful for
+ 'what did I do today' agent prompts.
+
+ workouts stats — per-exercise summaries across the window.
+ Markdown: one section per exercise with PR/recent and a per-month
+ bar chart of best weight (or duration for cardio).
+ JSON: array of ExerciseSummary { name, type, sessions: [SessionStats] }.
+ Filters: --exercise, --detail (per-session breakdown).
+
+ bodyweights list — recorded bodyweights.
+ Output: one line per entry, '2026-04-15 187.6 lbs'.
+
+ bodyweights stats — current/high/low, monthly trend chart, plateau
+ detection on the trailing 6 months.
+
+EXAMPLES
+
+ # Today's workout, scannable
+ liftoff-export workouts show today
+
+ # 30-day exercise volume, parsed
+ liftoff-export workouts stats --since 30d --format json | jq '
+ .[] | select(.type == "WR")
+ | { name, total_volume: ([.sessions[].volume] | add) }'
+
+ # PR over time for one exercise
+ liftoff-export workouts stats --exercise bench --since 1y --format json |
+ jq '.[].sessions | map({ date, weight: .bestWeight, reps: .bestReps })'
+
+ # Bodyweight delta vs 90 days ago
+ liftoff-export bodyweights list --since 90d --format json |
+ jq '[.[]] | (.[-1].weight - .[0].weight)'
+
+GOTCHAS
+ - Workout dates are LOCAL. A 11pm workout buckets on the date you
+ logged it, not the UTC date.
+ - Liftoff retires API hosts periodically — see LIFTOFF_API_BASE above.
+ 'liftoff-export auth status' won't catch this; the failure is a
+ deprecation message on the next subcommand call.
+ - Bodyweight is read off Post.bodyweight, which is the value you
+ entered for that workout — not a separate weigh-in feed. No workout
+ that day means no bodyweight that day.
+ - 'workouts stats' silently bins exercises by name. Renaming an
+ exercise in Liftoff splits it into two summaries.
+`
+
+var primeCmd = &cobra.Command{
+ Use: "prime",
+ Short: "Print an LLM-targeted primer (output formats, subcommands, jq recipes)",
+ Long: `Print a one-screen primer aimed at LLM agents calling this CLI as a tool.
+Covers the output formats (markdown by default, --format json for structured),
+auth subcommands and env vars, the subcommands and what their rows look like,
+the shared date flags, and a few jq recipes for common questions.`,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ _, err := fmt.Fprint(cmd.OutOrStdout(), primeText)
+ return err
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(primeCmd)
+}
diff --git a/cmd/root.go b/cmd/root.go
index df8b829..a1cdfcb 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -9,6 +9,12 @@ import (
var rootCmd = &cobra.Command{
Use: "liftoff-export",
Short: "CLI for the Liftoff fitness app",
+ Long: `liftoff-export reads your personal Liftoff (gymbros.com) workout and
+bodyweight data and prints it on stdout. Default output is narrow,
+fitdown-style markdown; pass --format json for the full structured row.
+
+LLM agents: run 'liftoff-export prime' for a one-screen orientation
+(I/O contract, subcommands, date flags, jq recipes).`,
}
func Execute() {