← back to Cli Printing Press
feat(templates): agent-friendly error messages, truncation hints, wired flags
64c58d863adb85499f32960375d75249dbc37ac2 · 2026-03-26 16:10:38 -0700 · Matt Van Horn
Apply Trevin Chow's "7 Principles for Agent-Friendly CLIs" to templates:
1. Actionable errors (Principle 3): error messages now include the
specific missing arg, the correct usage pattern, and the command path.
Agents can self-correct in one retry instead of guessing.
2. Bounded responses (Principle 7): list commands with 25+ results now
print "Showing N results. To narrow: add --limit, --json --select,
or filter flags." on stderr. Teaches agents how to get precise results.
3. Wired flags (Principles 2, 6): new printOutputWithFlags() dispatches
to --csv (printCSV with header row), --quiet (suppress output, exit
code only), and --select (filterFields). These flags were declared
in root.go but never checked in command RunE functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M internal/generator/templates/command_endpoint.go.tmplM internal/generator/templates/helpers.go.tmpl
Diff
commit 64c58d863adb85499f32960375d75249dbc37ac2
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Thu Mar 26 16:10:38 2026 -0700
feat(templates): agent-friendly error messages, truncation hints, wired flags
Apply Trevin Chow's "7 Principles for Agent-Friendly CLIs" to templates:
1. Actionable errors (Principle 3): error messages now include the
specific missing arg, the correct usage pattern, and the command path.
Agents can self-correct in one retry instead of guessing.
2. Bounded responses (Principle 7): list commands with 25+ results now
print "Showing N results. To narrow: add --limit, --json --select,
or filter flags." on stderr. Teaches agents how to get precise results.
3. Wired flags (Principles 2, 6): new printOutputWithFlags() dispatches
to --csv (printCSV with header row), --quiet (suppress output, exit
code only), and --select (filterFields). These flags were declared
in root.go but never checked in command RunE functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
.../generator/templates/command_endpoint.go.tmpl | 4 +-
internal/generator/templates/helpers.go.tmpl | 68 +++++++++++++++++++++-
2 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 0278dcf7..dd75e17f 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -52,7 +52,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- range $i, $p := .Endpoint.Params}}
{{- if .Positional}}
if len(args) < {{add $i 1}} {
- return usageErr(fmt.Errorf("{{.Name}} is required"))
+ return usageErr(fmt.Errorf("{{.Name}} is required\nUsage: %s %s <%s>", cmd.Root().Name(), cmd.CommandPath(), "{{.Name}}"))
}
path = replacePathParam(path, "{{.Name}}", args[{{$i}}])
{{- end}}
@@ -154,7 +154,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
}
{{- end}}
- return printOutput(cmd.OutOrStdout(), data, flags.asJSON)
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
},
}
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 63970861..0ea7e5d2 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -285,6 +285,65 @@ func filterFields(data json.RawMessage, fields string) json.RawMessage {
return data
}
+// printOutputWithFlags routes output through the right format based on flags.
+func printOutputWithFlags(w io.Writer, data json.RawMessage, flags *rootFlags) error {
+ // Apply --select field filtering
+ if flags.selectFields != "" {
+ data = filterFields(data, flags.selectFields)
+ }
+ // --quiet: suppress all output, exit code communicates result
+ if flags.quiet {
+ return nil
+ }
+ // --csv: render as CSV
+ if flags.csv {
+ return printCSV(w, data)
+ }
+ return printOutput(w, data, flags.asJSON)
+}
+
+// printCSV renders JSON arrays as CSV with header row.
+func printCSV(w io.Writer, data json.RawMessage) error {
+ var items []map[string]any
+ if err := json.Unmarshal(data, &items); err != nil || len(items) == 0 {
+ // Single object or empty - just print as JSON
+ fmt.Fprintln(w, string(data))
+ return nil
+ }
+ // Collect all keys for header
+ keySet := map[string]bool{}
+ for _, item := range items {
+ for k := range item {
+ keySet[k] = true
+ }
+ }
+ var keys []string
+ for k := range keySet {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ // Header
+ fmt.Fprintln(w, strings.Join(keys, ","))
+ // Rows
+ for _, item := range items {
+ var vals []string
+ for _, k := range keys {
+ v := item[k]
+ if v == nil {
+ vals = append(vals, "")
+ } else {
+ s := fmt.Sprintf("%v", v)
+ if strings.ContainsAny(s, ",\"\n") {
+ s = `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
+ }
+ vals = append(vals, s)
+ }
+ }
+ fmt.Fprintln(w, strings.Join(vals, ","))
+ }
+ return nil
+}
+
// printOutput auto-detects arrays and renders as tables, or prints raw JSON for objects.
func printOutput(w io.Writer, data json.RawMessage, asJSON bool) error {
if asJSON {
@@ -296,7 +355,14 @@ func printOutput(w io.Writer, data json.RawMessage, asJSON bool) error {
// Try to detect if response is an array
var items []map[string]any
if err := json.Unmarshal(data, &items); err == nil && len(items) > 0 {
- return printAutoTable(w, items)
+ if err := printAutoTable(w, items); err != nil {
+ return err
+ }
+ // Agent-friendly: show count and suggest narrowing when results are large
+ if len(items) >= 25 {
+ fmt.Fprintf(os.Stderr, "\nShowing %d results. To narrow: add --limit, --json --select, or filter flags.\n", len(items))
+ }
+ return nil
}
// Single object - pretty print
← 8c92c19d feat(graphql): add GraphQL SDL parser for CLI generation
·
back to Cli Printing Press
·
feat(templates): auto-JSON piping, --no-input, --compact (Ra 300c01bd →