diff --git a/README.md b/README.md index e443a72..56ccdd6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # crono-export-cli -Export your personal nutrition, biometric, and food-log data from [Cronometer](https://cronometer.com) as JSON. Built for personal LLM agents and scripts that want structured nutrition data — for example, an LLM-driven bariatric or fitness coach that needs to know how much protein, B12, iron, or calcium you actually got today. +Export your personal nutrition, biometric, and food-log data from [Cronometer](https://cronometer.com) as narrow markdown (the default) or JSON. Built for personal LLM agents and scripts that want structured nutrition data — for example, an LLM-driven bariatric or fitness coach that needs to know how much protein, B12, iron, or calcium you actually got today. [![Latest Release](https://img.shields.io/github/v/release/quantcli/crono-export-cli)](https://github.com/quantcli/crono-export-cli/releases/latest) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -10,7 +10,7 @@ Export your personal nutrition, biometric, and food-log data from [Cronometer](h ## Features - **Five export endpoints** — servings (per-food log with full nutrient breakdown), nutrition (daily totals), biometrics (weight, body fat, custom metrics), exercises, and notes -- **JSON on stdout** — pipe straight to `jq`, save to disk, or hand to an LLM tool +- **Markdown by default, JSON on demand** — narrow fitdown-style markdown reads well in chat and terminals; pass `--json` for the full structured row to pipe through `jq` - **Date selection** — `--today`, `--days N`, or `--start YYYY-MM-DD --end YYYY-MM-DD` on every subcommand - **Single static binary** — no Python or Node runtime; drop it in `~/bin/` and go - **Credentials via env** — `CRONOMETER_USERNAME` / `CRONOMETER_PASSWORD`, no config file needed @@ -101,24 +101,20 @@ crono-export servings --days 7 crono-export servings --start 2026-04-01 --end 2026-04-15 ``` -Sample row (truncated): - -```json -{ - "RecordedTime": "2026-04-11T00:00:00Z", - "Group": "Breakfast", - "FoodName": "Cheese Cracker", - "QuantityValue": 20, - "QuantityUnits": "square", - "EnergyKcal": 97.8, - "ProteinG": 1.95, - "CarbsG": 11.88, - "FiberG": 0.46, - "FatG": 4.94, - "B12Mg": 0.07, - "CalciumMg": 27.2, - "IronMg": 0.69 -} +Default markdown output (per food, zero-valued nutrients suppressed): + +```markdown +## 2026-04-11 + +### Breakfast · Cheese Cracker (20 square) +- Energy: 97.8 kcal +- Protein: 1.95 g +- Carbs: 11.88 g +- Fiber: 0.46 g +- Fat: 4.94 g +- B12: 0.07 mg +- Calcium: 27.2 mg +- Iron: 0.69 mg ``` ### Nutrition — daily totals @@ -135,15 +131,9 @@ crono-export nutrition --days 30 crono-export biometrics --days 30 ``` -```json -[ - { - "RecordedTime": "2026-04-10T00:00:00Z", - "Metric": "Weight", - "Unit": "lbs", - "Amount": 237 - } -] +```markdown +## 2026-04-10 +- Weight: 237 lbs ``` ### Exercises @@ -160,12 +150,17 @@ crono-export notes --days 30 ## Output Format -Every subcommand prints pretty-printed JSON to stdout. Errors and progress messages go to stderr, so it's safe to pipe stdout into `jq`, redirect into a file, or feed to an LLM tool without worrying about mixed output. +Default output is narrow, [Fitdown](https://github.com/datavis-tech/fitdown)-style markdown — date-grouped headings, one bullet per non-zero field, no wide tables. Markdown reads well in chat and on a terminal and is easy for an LLM to consume inline. + +For programmatic use, pass `--json` (or `--format json`) to get the full structured row as a JSON array on stdout — nothing suppressed, easy to pipe through `jq`. Errors always go to stderr, so JSON output stays clean for piping. ```sh -crono-export servings --today | jq '[.[] | {food: .FoodName, protein: .ProteinG}]' +crono-export servings --today # markdown, default +crono-export servings --today --json | jq '[.[] | {food: .FoodName, protein: .ProteinG}]' ``` +LLM agents: run `crono-export prime` for a one-screen orientation describing both formats, all subcommands, the date flags, and `jq` recipes. + ## About Cronometer [Cronometer](https://cronometer.com) is a nutrition tracking app with one of the best micronutrient databases of any consumer tool — a major reason it's commonly recommended for bariatric patients, anyone tracking specific vitamin/mineral targets, or athletes managing recovery nutrition. diff --git a/cmd/biometrics.go b/cmd/biometrics.go index 44dda76..cc5bf6b 100644 --- a/cmd/biometrics.go +++ b/cmd/biometrics.go @@ -24,11 +24,12 @@ var biometricsCmd = &cobra.Command{ if err != nil { return err } - return emitJSON(recs) + return emit(cmd, kindBiometrics, recs) }, } func init() { cronoclient.AddDateRangeFlags(biometricsCmd) + AddFormatFlags(biometricsCmd) rootCmd.AddCommand(biometricsCmd) } diff --git a/cmd/exercises.go b/cmd/exercises.go index d68288c..9013a7b 100644 --- a/cmd/exercises.go +++ b/cmd/exercises.go @@ -24,11 +24,12 @@ var exercisesCmd = &cobra.Command{ if err != nil { return err } - return emitJSON(recs) + return emit(cmd, kindExercises, recs) }, } func init() { cronoclient.AddDateRangeFlags(exercisesCmd) + AddFormatFlags(exercisesCmd) rootCmd.AddCommand(exercisesCmd) } diff --git a/cmd/format.go b/cmd/format.go new file mode 100644 index 0000000..c5ad521 --- /dev/null +++ b/cmd/format.go @@ -0,0 +1,375 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "os" + "reflect" + "sort" + "strings" + + "github.com/jrmycanady/gocronometer" + "github.com/spf13/cobra" +) + +type recordKind int + +const ( + kindServings recordKind = iota + kindNutrition + kindBiometrics + kindExercises + kindNotes +) + +// AddFormatFlags registers --format and --json on every export subcommand. +func AddFormatFlags(cmd *cobra.Command) { + cmd.Flags().String("format", "md", "output format: md|json") + cmd.Flags().Bool("json", false, "shortcut for --format json") +} + +func chosenFormat(cmd *cobra.Command) (string, error) { + f, _ := cmd.Flags().GetString("format") + if j, _ := cmd.Flags().GetBool("json"); j { + f = "json" + } + switch f { + case "md", "markdown": + return "md", nil + case "json": + return "json", nil + default: + return "", fmt.Errorf("unknown --format %q (want md or json)", f) + } +} + +// emit writes v in the format chosen on cmd. kind tells the markdown +// renderer which layout to use. +func emit(cmd *cobra.Command, kind recordKind, v any) error { + f, err := chosenFormat(cmd) + if err != nil { + return err + } + if f == "json" { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(v) + } + return renderMarkdown(os.Stdout, kind, v) +} + +func renderMarkdown(w io.Writer, kind recordKind, v any) error { + switch kind { + case kindServings: + recs, _ := v.(gocronometer.ServingRecords) + return renderServings(w, recs) + case kindBiometrics: + recs, _ := v.(gocronometer.BiometricRecords) + return renderBiometrics(w, recs) + case kindExercises: + recs, _ := v.(gocronometer.ExerciseRecords) + return renderExercises(w, recs) + case kindNutrition: + rows, _ := v.([]map[string]string) + return renderNutrition(w, rows) + case kindNotes: + rows, _ := v.([]map[string]string) + return renderNotes(w, rows) + } + return fmt.Errorf("renderMarkdown: unknown kind %d", kind) +} + +// ---- shared helpers --------------------------------------------------- + +func emptyMsg(w io.Writer) error { + _, err := fmt.Fprintln(w, "_(no records in window)_") + return err +} + +// fmtFloat trims trailing zeros so 1.95 → "1.95" and 100.000 → "100". +func fmtFloat(f float64) string { + s := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.4f", f), "0"), ".") + if s == "" || s == "-" { + return "0" + } + return s +} + +// strippedSuffix splits a CamelCase Go field name like "EnergyKcal" or +// "B12Mg" into ("Energy","kcal") or ("B12","mg"). Order matters: longer +// suffixes first so we don't strip "g" out of "Mg". +func strippedSuffix(field string) (name, unit string) { + for _, suf := range []struct{ go_, display string }{ + {"Kcal", "kcal"}, + {"Mg", "mg"}, + {"Ug", "µg"}, + {"UI", "IU"}, + {"G", "g"}, + } { + if strings.HasSuffix(field, suf.go_) && len(field) > len(suf.go_) { + return field[:len(field)-len(suf.go_)], suf.display + } + } + return field, "" +} + +// ---- servings --------------------------------------------------------- + +func renderServings(w io.Writer, recs gocronometer.ServingRecords) error { + if len(recs) == 0 { + return emptyMsg(w) + } + // Group by local calendar date. + byDate := map[string][]gocronometer.ServingRecord{} + for _, r := range recs { + d := r.RecordedTime.Format("2006-01-02") + byDate[d] = append(byDate[d], r) + } + dates := make([]string, 0, len(byDate)) + for d := range byDate { + dates = append(dates, d) + } + sort.Strings(dates) + + for di, d := range dates { + if di > 0 { + fmt.Fprintln(w) + } + fmt.Fprintf(w, "## %s\n\n", d) + for _, r := range byDate[d] { + renderServingRecord(w, r) + } + } + fmt.Fprintln(w, "_zero-valued nutrients omitted; use --json for the full row_") + return nil +} + +func renderServingRecord(w io.Writer, r gocronometer.ServingRecord) { + header := fmt.Sprintf("### %s · %s", strDefault(r.Group, "—"), r.FoodName) + if r.QuantityValue != 0 || r.QuantityUnits != "" { + header += fmt.Sprintf(" (%s %s)", fmtFloat(r.QuantityValue), r.QuantityUnits) + } + fmt.Fprintln(w, header) + + v := reflect.ValueOf(r) + t := v.Type() + skip := map[string]bool{ + "RecordedTime": true, + "Group": true, + "FoodName": true, + "QuantityValue": true, + "QuantityUnits": true, + "Category": true, + } + for i := 0; i < t.NumField(); i++ { + fname := t.Field(i).Name + if skip[fname] { + continue + } + if v.Field(i).Kind() != reflect.Float64 { + continue + } + val := v.Field(i).Float() + if val == 0 { + continue + } + name, unit := strippedSuffix(fname) + if unit != "" { + fmt.Fprintf(w, "- %s: %s %s\n", name, fmtFloat(val), unit) + } else { + fmt.Fprintf(w, "- %s: %s\n", name, fmtFloat(val)) + } + } + fmt.Fprintln(w) +} + +func strDefault(s, fallback string) string { + if s == "" { + return fallback + } + return s +} + +// ---- biometrics ------------------------------------------------------- + +func renderBiometrics(w io.Writer, recs gocronometer.BiometricRecords) error { + if len(recs) == 0 { + return emptyMsg(w) + } + byDate := map[string][]gocronometer.BiometricRecord{} + for _, r := range recs { + d := r.RecordedTime.Format("2006-01-02") + byDate[d] = append(byDate[d], r) + } + dates := make([]string, 0, len(byDate)) + for d := range byDate { + dates = append(dates, d) + } + sort.Strings(dates) + for di, d := range dates { + if di > 0 { + fmt.Fprintln(w) + } + fmt.Fprintf(w, "## %s\n", d) + for _, r := range byDate[d] { + unit := r.Unit + if unit != "" { + unit = " " + unit + } + fmt.Fprintf(w, "- %s: %s%s\n", r.Metric, fmtFloat(r.Amount), unit) + } + } + return nil +} + +// ---- exercises -------------------------------------------------------- + +func renderExercises(w io.Writer, recs gocronometer.ExerciseRecords) error { + if len(recs) == 0 { + return emptyMsg(w) + } + byDate := map[string][]gocronometer.ExerciseRecord{} + for _, r := range recs { + d := r.RecordedTime.Format("2006-01-02") + byDate[d] = append(byDate[d], r) + } + dates := make([]string, 0, len(byDate)) + for d := range byDate { + dates = append(dates, d) + } + sort.Strings(dates) + for di, d := range dates { + if di > 0 { + fmt.Fprintln(w) + } + fmt.Fprintf(w, "## %s\n", d) + for _, r := range byDate[d] { + parts := []string{r.Exercise} + if r.Minutes != 0 { + parts = append(parts, fmtFloat(r.Minutes)+" min") + } + if r.CaloriesBurned != 0 { + parts = append(parts, fmtFloat(r.CaloriesBurned)+" kcal") + } + line := strings.Join(parts, ", ") + if r.Group != "" { + line += fmt.Sprintf(" (%s)", r.Group) + } + fmt.Fprintf(w, "- %s\n", line) + } + } + return nil +} + +// ---- nutrition (daily totals, string-keyed CSV) ---------------------- + +func renderNutrition(w io.Writer, rows []map[string]string) error { + if len(rows) == 0 { + return emptyMsg(w) + } + // Sort by Date asc. + sort.SliceStable(rows, func(i, j int) bool { + return rows[i]["Date"] < rows[j]["Date"] + }) + for di, row := range rows { + if di > 0 { + fmt.Fprintln(w) + } + date := row["Date"] + if date == "" { + date = "(unknown date)" + } + fmt.Fprintf(w, "## %s\n", date) + + keys := make([]string, 0, len(row)) + for k := range row { + if k == "Date" { + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := row[k] + if isZeroish(v) { + continue + } + fmt.Fprintf(w, "- %s: %s\n", k, v) + } + } + fmt.Fprintln(w) + fmt.Fprintln(w, "_zero-valued nutrients omitted; use --json for the full row_") + return nil +} + +// isZeroish reports whether a CSV value should be treated as "no data" and +// hidden from the markdown output. Empty strings, "0", "0.0", "0.00", etc. +// are zeroish; everything else (including "false", "true", arbitrary text) +// is rendered. +func isZeroish(s string) bool { + if s == "" { + return true + } + // Try numeric: if it parses to 0, it's zeroish. + var f float64 + if _, err := fmt.Sscanf(s, "%f", &f); err == nil && f == 0 { + // But only if the entire string was numeric. + t := strings.TrimSpace(s) + for _, r := range t { + if !(r >= '0' && r <= '9') && r != '.' && r != '-' && r != '+' { + return false + } + } + return true + } + return false +} + +// ---- notes ------------------------------------------------------------ + +func renderNotes(w io.Writer, rows []map[string]string) error { + if len(rows) == 0 { + return emptyMsg(w) + } + dateKey := pickKey(rows[0], "Day", "Date") + noteKey := pickKey(rows[0], "Note", "Notes", "Comment") + timeKey := pickKey(rows[0], "Time") + + for di, row := range rows { + if di > 0 { + fmt.Fprintln(w) + } + date := row[dateKey] + if date == "" { + date = "(unknown date)" + } + header := "## " + date + if t := row[timeKey]; t != "" { + header += " " + t + } + fmt.Fprintln(w, header) + if note := strings.TrimSpace(row[noteKey]); note != "" { + fmt.Fprintln(w, note) + } else { + // Fall back to dumping all non-empty fields if we can't find a Note column. + for k, v := range row { + if k == dateKey || k == timeKey || v == "" { + continue + } + fmt.Fprintf(w, "- %s: %s\n", k, v) + } + } + } + return nil +} + +func pickKey(row map[string]string, candidates ...string) string { + for _, c := range candidates { + if _, ok := row[c]; ok { + return c + } + } + return "" +} + diff --git a/cmd/notes.go b/cmd/notes.go index 5e91bbd..734ac84 100644 --- a/cmd/notes.go +++ b/cmd/notes.go @@ -24,11 +24,12 @@ var notesCmd = &cobra.Command{ if err != nil { return err } - return emitJSON(rows) + return emit(cmd, kindNotes, rows) }, } func init() { cronoclient.AddDateRangeFlags(notesCmd) + AddFormatFlags(notesCmd) rootCmd.AddCommand(notesCmd) } diff --git a/cmd/nutrition.go b/cmd/nutrition.go index 39b5b6a..68ecb9e 100644 --- a/cmd/nutrition.go +++ b/cmd/nutrition.go @@ -24,11 +24,12 @@ var nutritionCmd = &cobra.Command{ if err != nil { return err } - return emitJSON(rows) + return emit(cmd, kindNutrition, rows) }, } func init() { cronoclient.AddDateRangeFlags(nutritionCmd) + AddFormatFlags(nutritionCmd) rootCmd.AddCommand(nutritionCmd) } diff --git a/cmd/prime.go b/cmd/prime.go index cf533f0..07506d2 100644 --- a/cmd/prime.go +++ b/cmd/prime.go @@ -11,15 +11,21 @@ const primeText = `crono-export — primer for LLM agents WHAT IT IS A CLI that reads your personal Cronometer data (per-food log, daily totals, - weight/biometrics, exercises, notes) and prints it as JSON on stdout. + weight/biometrics, exercises, notes) and prints it on stdout. -I/O CONTRACT - - Output: pretty-printed JSON ARRAY on stdout. THIS IS THE ONLY MODE. - There is no --json / --format flag; JSON is always the format. - - Errors: human-readable text on stderr. You do NOT need '2>&1'. - - Empty result '[]' = success with zero rows in the window, not an error. - - Exit code: 0 success, non-zero on auth or network failure. - - Filter with jq. Don't pipe to python — output is already JSON. +OUTPUT FORMATS + Default: narrow, fitdown-style markdown — date-grouped headings, one + bullet per non-zero field, easy to skim and easy for an LLM to consume + inline. Zero-valued nutrients are suppressed in markdown. + + --json (or --format json) Pretty-printed JSON ARRAY of full rows. + Use this when you want the complete row, + when piping to jq, or when round-tripping + into other tools. Nothing is suppressed. + + 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 records in window)_", JSON prints '[]'. AUTH Set both env vars before invoking. No config file or token cache; the CLI @@ -36,62 +42,71 @@ DATE FLAGS (every export subcommand accepts these) SUBCOMMANDS servings — per-food log: one row per food eaten, full nutrient breakdown. - Typed numbers. Keys (subset): + Markdown: ## per date, ### per food (group · name · quantity), bullets + for non-zero nutrients. + JSON: typed numbers. Keys (subset): RecordedTime, Group, FoodName, QuantityValue, QuantityUnits, EnergyKcal, ProteinG, CarbsG, FiberG, FatG, SodiumMg, CalciumMg, - IronMg, B12Mg, VitaminDUI, Omega3G, Omega6G, ... (60+ nutrients). + IronMg, B12Mg, VitaminDUI, Omega3G, ... (60+ nutrients). nutrition — daily totals: one row per day across all foods logged that day. - String-keyed (raw CSV columns, ALL VALUES ARE STRINGS — cast in jq). + Markdown: ## per date, bullets for non-zero columns. + JSON: string-keyed (raw CSV columns, ALL VALUES ARE STRINGS — cast in jq). Keys (subset): "Date", "Energy (kcal)", "Protein (g)", "Carbs (g)", "Fat (g)", "Fiber (g)", "Sodium (mg)", "Iron (mg)", "Calcium (mg)", "B12 (Cobalamin) (µg)", "Cholesterol (mg)", "Completed", ... biometrics — weight, body fat, blood pressure, custom metrics. - Typed. Keys: RecordedTime, Metric, Unit, Amount. + Markdown: ## per date, bullet per metric: "- Metric: amount unit". + JSON keys: RecordedTime, Metric, Unit, Amount. exercises — logged cardio / strength / custom activities. - Typed. Keys: RecordedTime, Exercise, Minutes, CaloriesBurned, Group. + Markdown: ## per date, one bullet per session. + JSON keys: RecordedTime, Exercise, Minutes, CaloriesBurned, Group. - notes — user-entered notes per day. String-keyed (raw CSV). + notes — user-entered notes per day. Markdown: ## per date with note + body. JSON: string-keyed (raw CSV). EXAMPLES - # Today's macros, as numbers - crono-export nutrition --today | jq '.[] | { + # Today's macros, scannable + crono-export nutrition --today + + # Today's macros, parsed (numbers via tonumber) + crono-export nutrition --today --json | jq '.[] | { date: .Date, kcal: (."Energy (kcal)" | tonumber), - protein: (."Protein (g)" | tonumber), - carbs: (."Carbs (g)" | tonumber), - fat: (."Fat (g)" | tonumber) + protein: (."Protein (g)" | tonumber) }' # 7-day protein total (servings is typed — no tonumber needed) - crono-export servings --days 7 | jq '[.[] | .ProteinG] | add' + crono-export servings --days 7 --json | jq '[.[] | .ProteinG] | add' # All foods from today's breakfast - crono-export servings --today | jq '[.[] | select(.Group == "Breakfast") | .FoodName]' + crono-export servings --today --json | jq '[.[] | select(.Group == "Breakfast") | .FoodName]' # Latest weight reading in a 30-day window - crono-export biometrics --days 30 | jq 'map(select(.Metric == "Weight")) | sort_by(.RecordedTime) | last' + crono-export biometrics --days 30 --json | jq 'map(select(.Metric == "Weight")) | sort_by(.RecordedTime) | last' GOTCHAS - "Today" is your LOCAL calendar day, not UTC. - - 'nutrition' and 'notes' values are STRINGS (raw CSV) — cast with - 'jq tonumber' when doing math. 'servings', 'biometrics', 'exercises' - are already typed numbers. - - Cronometer logs by calendar day; nothing here is real-time. The same + - 'nutrition' and 'notes' JSON values are STRINGS (raw CSV) — cast with + 'jq tonumber' when doing math. 'servings', 'biometrics', 'exercises' + JSON values are already typed numbers. + - Markdown drops zero-valued nutrients to stay readable. If you need + every column (including zeros), use --json. + - Cronometer logs by calendar day; nothing here is real-time. The same --today call moments apart returns the same data. ` var primeCmd = &cobra.Command{ Use: "prime", - Short: "Print an LLM-targeted primer (I/O contract, subcommands, jq recipes)", + 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 contract (JSON-on-stdout, no --json flag), auth env vars, -the subcommands and what their rows look like, the shared date flags, and a -few jq recipes for common questions.`, +Covers the output formats (markdown by default, --json for structured), +auth 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 diff --git a/cmd/root.go b/cmd/root.go index 06947ff..6c81783 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( - "encoding/json" "fmt" "os" @@ -11,16 +10,18 @@ import ( var rootCmd = &cobra.Command{ Use: "crono-export", - Short: "Export Cronometer nutrition, biometrics, and food log data as JSON", + Short: "Export Cronometer nutrition, biometrics, and food log data", Long: `crono-export reads your personal Cronometer data via the same export -endpoints the web app uses and prints it as JSON on stdout. +endpoints the web app uses and prints it on stdout. Default output is +narrow, fitdown-style markdown; pass --json (or --format json) for the +full structured row. Credentials must be supplied via environment variables: CRONOMETER_USERNAME your Cronometer email CRONOMETER_PASSWORD your Cronometer password -Designed for use by personal LLM agents and scripts that want structured -nutrition data — for example, an LLM-driven bariatric or fitness coach. +Designed for use by personal LLM agents and scripts — markdown reads +well in chat, JSON pipes well to jq. LLM agents: run 'crono-export prime' for a one-screen orientation (I/O contract, subcommands, date flags, jq recipes).`, @@ -35,10 +36,3 @@ func Execute() { os.Exit(1) } } - -// emitJSON pretty-prints v as JSON to stdout. Used by every subcommand. -func emitJSON(v any) error { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - return enc.Encode(v) -} diff --git a/cmd/servings.go b/cmd/servings.go index fb51ccb..605e1e4 100644 --- a/cmd/servings.go +++ b/cmd/servings.go @@ -24,11 +24,12 @@ var servingsCmd = &cobra.Command{ if err != nil { return err } - return emitJSON(recs) + return emit(cmd, kindServings, recs) }, } func init() { cronoclient.AddDateRangeFlags(servingsCmd) + AddFormatFlags(servingsCmd) rootCmd.AddCommand(servingsCmd) }