[object Object]

← 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

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 →