From e2539199c9af1f8ec04c9245137651fff6d81598 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sat, 14 Feb 2026 14:09:43 -0600 Subject: [PATCH 1/6] feat(agent): add agent operability overhaul for AI coding assistants Make pup the best CLI tool for AI agents to interact with Datadog by minimizing round trips, maximizing information density, and providing structured output that agents can parse without guessing. Phase 1 - Agent Detection & Mode Infrastructure: - Expand agent detection to 11+ AI coding assistants (Claude Code, Cursor, Codex, Aider, Cline, Windsurf, GitHub Copilot, Amazon Q, Gemini Code, Sourcegraph Cody, OpenCode) - Add IsAgentMode() and DetectAgentInfo() exported functions - Add DD_AGENT_MODE=1 explicit override env var - Add --agent persistent flag to root command - Add AgentMode field to Config struct - Auto-approve prompts in agent mode to prevent stdin hangs Phase 2 - Agent Help Schema (--hlp): - Add --hlp flag that outputs complete command schema as structured JSON - Schema includes all commands, flags, query syntax, time formats, workflows, best practices, and anti-patterns in a single call - Support subtree schemas: 'pup logs --hlp' returns only logs-related info - Eliminates multi-round-trip help lookups (was: --help per command) Phase 3 - Agent Command Group: - Add 'pup agent schema' command (same as --hlp) - Add 'pup agent schema --compact' for minimal token-efficient output - Add 'pup agent guide' with comprehensive embedded steering document - Add 'pup agent guide ' for domain-specific sections - Add "agent" to reserved command list in alias.go Phase 4 - Structured Output: - Add AgentEnvelope type wrapping responses with metadata (count, truncated, next_action hints, warnings) - Add AgentError type for structured error responses with suggestions - Add formatAndPrint() helper that wraps output in agent mode, passes through in human mode - Migrate monitors list/get, logs search, metrics query to formatAndPrint - Structured errors include status code-specific suggestions (401->re-auth, etc.) Phase 5 - Smart Defaults: - Agent mode: monitors list default limit 500 (vs 200) - Agent mode: logs search default limit 200 (vs 50) - Human mode behavior is completely unchanged Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/agent.go | 141 ++++++++++++++ cmd/agent_test.go | 274 +++++++++++++++++++++++++++ cmd/alias.go | 1 + cmd/logs_simple.go | 26 ++- cmd/metrics.go | 10 +- cmd/monitors.go | 43 +++-- cmd/root.go | 149 +++++++++++++-- pkg/agenthelp/agenthelp.go | 231 +++++++++++++++++++++++ pkg/agenthelp/agenthelp_test.go | 204 ++++++++++++++++++++ pkg/agenthelp/guide.go | 44 +++++ pkg/agenthelp/guide.md | 318 +++++++++++++++++++++++++++++++ pkg/agenthelp/guide_test.go | 59 ++++++ pkg/agenthelp/steering.go | 112 +++++++++++ pkg/agenthelp/steering_test.go | 74 ++++++++ pkg/config/config.go | 1 + pkg/formatter/envelope.go | 82 ++++++++ pkg/formatter/envelope_test.go | 150 +++++++++++++++ pkg/useragent/useragent.go | 73 +++++-- pkg/useragent/useragent_test.go | 324 +++++++++++++++----------------- 19 files changed, 2086 insertions(+), 230 deletions(-) create mode 100644 cmd/agent.go create mode 100644 cmd/agent_test.go create mode 100644 pkg/agenthelp/agenthelp.go create mode 100644 pkg/agenthelp/agenthelp_test.go create mode 100644 pkg/agenthelp/guide.go create mode 100644 pkg/agenthelp/guide.md create mode 100644 pkg/agenthelp/guide_test.go create mode 100644 pkg/agenthelp/steering.go create mode 100644 pkg/agenthelp/steering_test.go create mode 100644 pkg/formatter/envelope.go create mode 100644 pkg/formatter/envelope_test.go diff --git a/cmd/agent.go b/cmd/agent.go new file mode 100644 index 00000000..56b1d1d6 --- /dev/null +++ b/cmd/agent.go @@ -0,0 +1,141 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/DataDog/pup/pkg/agenthelp" + "github.com/spf13/cobra" +) + +var agentSchemaCompact bool + +var agentCmd = &cobra.Command{ + Use: "agent", + Short: "Agent tooling: schema, guide, and diagnostics for AI coding assistants", + Long: `Commands for AI coding assistants to interact with pup efficiently. + +COMMANDS: + schema Output the complete command schema as JSON (same as --hlp) + guide Output the comprehensive steering guide + +EXAMPLES: + # Get full JSON schema (all commands, flags, query syntax) + pup agent schema + + # Get compact schema (command names and flags only, fewer tokens) + pup agent schema --compact + + # Get the steering guide + pup agent guide + + # Get guide for a specific domain + pup agent guide logs`, +} + +var agentSchemaCmd = &cobra.Command{ + Use: "schema", + Short: "Output command schema as JSON", + Long: `Output the complete pup command schema as structured JSON. + +This is the same output as 'pup --hlp' and includes all commands, flags, +query syntax, time formats, workflows, best practices, and anti-patterns. + +FLAGS: + --compact Output minimal schema (command names and flags only) + +EXAMPLES: + pup agent schema + pup agent schema --compact`, + RunE: runAgentSchema, +} + +var agentGuideCmd = &cobra.Command{ + Use: "guide [domain]", + Short: "Output the comprehensive steering guide", + Long: `Output the pup steering guide for AI coding assistants. + +Without arguments, outputs the full guide. With a domain argument, +outputs only the section relevant to that domain. + +EXAMPLES: + pup agent guide + pup agent guide logs + pup agent guide metrics + pup agent guide monitors + pup agent guide apm`, + Args: cobra.MaximumNArgs(1), + RunE: runAgentGuide, +} + +func init() { + agentSchemaCmd.Flags().BoolVar(&agentSchemaCompact, "compact", false, "Output minimal schema (names + flags only)") + + agentCmd.AddCommand(agentSchemaCmd) + agentCmd.AddCommand(agentGuideCmd) +} + +func runAgentSchema(cmd *cobra.Command, args []string) error { + root := cmd.Root() + + var data interface{} + if agentSchemaCompact { + data = agenthelp.GenerateCompactSchema(root) + } else { + data = agenthelp.GenerateSchema(root) + } + + out, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal schema: %w", err) + } + + printOutput("%s\n", string(out)) + return nil +} + +func runAgentGuide(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + printOutput("%s\n", agenthelp.GetGuideSection(args[0])) + return nil + } + printOutput("%s\n", agenthelp.GetGuide()) + return nil +} + +// HandleHlpFlag processes the --hlp flag on any command. +// It generates the schema for the full tree or a subtree and exits. +// Returns true if --hlp was handled (caller should return). +func HandleHlpFlag(cmd *cobra.Command) (bool, error) { + if !hlpFlag { + return false, nil + } + + root := cmd.Root() + + var data interface{} + // If --hlp is on a subcommand, generate subtree schema + if cmd != root && cmd.Parent() == root { + schema := agenthelp.GenerateSubtreeSchema(root, cmd.Name()) + if schema != nil { + data = schema + } else { + data = agenthelp.GenerateSchema(root) + } + } else { + data = agenthelp.GenerateSchema(root) + } + + out, err := json.MarshalIndent(data, "", " ") + if err != nil { + return true, fmt.Errorf("failed to marshal schema: %w", err) + } + + printOutput("%s\n", string(out)) + return true, nil +} diff --git a/cmd/agent_test.go b/cmd/agent_test.go new file mode 100644 index 00000000..0099d7d4 --- /dev/null +++ b/cmd/agent_test.go @@ -0,0 +1,274 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package cmd + +import ( + "bytes" + "encoding/json" + "os" + "strings" + "testing" + + "github.com/DataDog/pup/pkg/agenthelp" + "github.com/DataDog/pup/pkg/config" +) + +func TestAgentSchema(t *testing.T) { + // Save and restore originals + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + err := ExecuteWithArgs([]string{"agent", "schema"}) + if err != nil { + t.Fatalf("agent schema error: %v", err) + } + + output := buf.String() + if output == "" { + t.Fatal("agent schema should produce output") + } + + // Verify it's valid JSON + var schema agenthelp.Schema + if err := json.Unmarshal([]byte(output), &schema); err != nil { + t.Fatalf("agent schema output is not valid JSON: %v", err) + } + + if schema.Version == "" { + t.Error("schema version should not be empty") + } + if len(schema.Commands) == 0 { + t.Error("schema commands should not be empty") + } + if len(schema.QuerySyntax) == 0 { + t.Error("schema query_syntax should not be empty") + } +} + +func TestAgentSchemaCompact(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + err := ExecuteWithArgs([]string{"agent", "schema", "--compact"}) + if err != nil { + t.Fatalf("agent schema --compact error: %v", err) + } + + output := buf.String() + + var compact agenthelp.CompactSchema + if err := json.Unmarshal([]byte(output), &compact); err != nil { + t.Fatalf("agent schema --compact output is not valid JSON: %v", err) + } + + if compact.Version == "" { + t.Error("compact schema version should not be empty") + } + if len(compact.Commands) == 0 { + t.Error("compact schema commands should not be empty") + } +} + +func TestAgentGuide(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + err := ExecuteWithArgs([]string{"agent", "guide"}) + if err != nil { + t.Fatalf("agent guide error: %v", err) + } + + output := buf.String() + if !strings.Contains(output, "Pup Agent Guide") { + t.Error("agent guide should contain the title") + } +} + +func TestAgentGuideDomain(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + err := ExecuteWithArgs([]string{"agent", "guide", "logs"}) + if err != nil { + t.Fatalf("agent guide logs error: %v", err) + } + + output := buf.String() + if !strings.Contains(output, "Logs") { + t.Error("agent guide logs should contain Logs content") + } +} + +func TestHlpFlag(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + err := ExecuteWithArgs([]string{"--hlp"}) + if err != nil { + t.Fatalf("--hlp error: %v", err) + } + + output := buf.String() + + var schema agenthelp.Schema + if err := json.Unmarshal([]byte(output), &schema); err != nil { + t.Fatalf("--hlp output is not valid JSON: %v", err) + } + + if len(schema.Commands) == 0 { + t.Error("--hlp schema commands should not be empty") + } +} + +func TestHlpFlagSubtree(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + err := ExecuteWithArgs([]string{"monitors", "--hlp"}) + if err != nil { + t.Fatalf("monitors --hlp error: %v", err) + } + + output := buf.String() + + var schema agenthelp.Schema + if err := json.Unmarshal([]byte(output), &schema); err != nil { + t.Fatalf("monitors --hlp output is not valid JSON: %v", err) + } + + if len(schema.Commands) != 1 { + t.Errorf("monitors --hlp should have 1 command, got %d", len(schema.Commands)) + } + if schema.Commands[0].Name != "monitors" { + t.Errorf("monitors --hlp command name = %q, want 'monitors'", schema.Commands[0].Name) + } +} + +func TestAgentModeAutoDetect(t *testing.T) { + origCfg := cfg + origClient := ddClient + origWriter := outputWriter + defer func() { + cfg = origCfg + ddClient = origClient + outputWriter = origWriter + os.Unsetenv("CLAUDECODE") + }() + + var buf bytes.Buffer + outputWriter = &buf + ddClient = nil + + // Set CLAUDECODE to trigger agent mode + os.Setenv("CLAUDECODE", "1") + + // Reset cfg so initConfig runs with the env var + cfg = nil + + // initConfig is called by cobra.OnInitialize, simulate it + initConfig() + + if !cfg.AgentMode { + t.Error("AgentMode should be true when CLAUDECODE=1") + } + if !cfg.AutoApprove { + t.Error("AutoApprove should be true in agent mode") + } +} + +func TestAgentModeFlagOverride(t *testing.T) { + origCfg := cfg + origClient := ddClient + origWriter := outputWriter + origAgentFlag := agentFlag + defer func() { + cfg = origCfg + ddClient = origClient + outputWriter = origWriter + agentFlag = origAgentFlag + }() + + var buf bytes.Buffer + outputWriter = &buf + ddClient = nil + + agentFlag = true + cfg = nil + initConfig() + + if !cfg.AgentMode { + t.Error("AgentMode should be true when --agent flag is set") + } + if !cfg.AutoApprove { + t.Error("AutoApprove should be true when --agent flag is set") + } +} diff --git a/cmd/alias.go b/cmd/alias.go index 7689580f..501ea53a 100644 --- a/cmd/alias.go +++ b/cmd/alias.go @@ -284,6 +284,7 @@ func isReservedCommand(name string) bool { "usage", "cost", "data-governance", "obs-pipelines", "network", "cloud", "integrations", "misc", "investigations", "product-analytics", "cases", "apm", + "agent", } // Convert to lowercase for case-insensitive comparison diff --git a/cmd/logs_simple.go b/cmd/logs_simple.go index b51b7167..47ee0450 100644 --- a/cmd/logs_simple.go +++ b/cmd/logs_simple.go @@ -727,6 +727,11 @@ func parseComputeString(compute string) (aggregation string, metric string, err // Implementation functions func runLogsSearch(cmd *cobra.Command, args []string) error { + // In agent mode, use a larger default limit (200) unless explicitly set + if isAgentMode() && !cmd.Flags().Changed("limit") { + logsLimit = 200 + } + // Validate storage tier before creating client storageTier, err := validateAndConvertStorageTier(logsStorage) if err != nil { @@ -866,16 +871,25 @@ func runLogsSearch(cmd *cobra.Command, args []string) error { finalResp := resp if pageCount > 1 { finalResp.SetData(allLogs) - printOutput("Fetched %d logs across %d pages\n\n", len(allLogs), pageCount) + if !isAgentMode() { + printOutput("Fetched %d logs across %d pages\n\n", len(allLogs), pageCount) + } } - output, err := formatter.FormatOutput(finalResp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err + count := len(allLogs) + var meta *formatter.Metadata + if isAgentMode() { + meta = &formatter.Metadata{ + Count: &count, + Command: "logs search", + } + if count == logsLimit { + meta.Truncated = true + meta.NextAction = fmt.Sprintf("Results may be truncated at %d. Use --limit=%d or narrow the --query", logsLimit, logsLimit*2) + } } - printOutput("%s\n", output) - return nil + return formatAndPrint(finalResp, meta) } func runLogsList(cmd *cobra.Command, args []string) error { diff --git a/cmd/metrics.go b/cmd/metrics.go index df419333..ce7a48cb 100644 --- a/cmd/metrics.go +++ b/cmd/metrics.go @@ -583,13 +583,11 @@ func runMetricsQuery(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to query metrics: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err + var meta *formatter.Metadata + if isAgentMode() { + meta = &formatter.Metadata{Command: "metrics query"} } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, meta) } // runMetricsSearch executes the metrics search command using the v1 API diff --git a/cmd/monitors.go b/cmd/monitors.go index 27dacf54..e90c3f95 100644 --- a/cmd/monitors.go +++ b/cmd/monitors.go @@ -6,6 +6,8 @@ package cmd import ( + "fmt" + "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" @@ -282,8 +284,11 @@ func runMonitorsList(cmd *cobra.Command, args []string) error { opts.WithMonitorTags(monitorTags) } - // Set limit for results (default 200, max 1000) - // Users can increase limit or use filters to find specific monitors + // In agent mode, use a larger default limit (500) unless explicitly set + if isAgentMode() && !cmd.Flags().Changed("limit") { + monitorLimit = 500 + } + if monitorLimit > 1000 { monitorLimit = 1000 } @@ -316,18 +321,26 @@ func runMonitorsList(cmd *cobra.Command, args []string) error { resp = resp[:monitorLimit] } - // Convert to interface{} to ensure compatibility with formatter - var data interface{} = resp + count := len(resp) + truncated := originalCount > int(monitorLimit) + var meta *formatter.Metadata + if isAgentMode() { + meta = &formatter.Metadata{ + Count: &count, + Truncated: truncated, + Command: "monitors list", + } + if truncated { + meta.NextAction = fmt.Sprintf("Use --limit=%d or refine with --tags/--name filters", min(int(monitorLimit)*2, 1000)) + } + } - output, err := formatter.FormatOutput(data, formatter.OutputFormat(outputFormat)) - if err != nil { + if err := formatAndPrint(resp, meta); err != nil { return err } - printOutput("%s\n", output) - - // Show count info if we're truncating - if originalCount > int(monitorLimit) { + // Show count info if we're truncating (human mode only) + if !isAgentMode() && truncated { printOutput("\nShowing %d of %d monitors (use --limit to adjust)\n", monitorLimit, originalCount) } @@ -348,13 +361,11 @@ func runMonitorsGet(cmd *cobra.Command, args []string) error { return formatAPIError("get monitor", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err + var meta *formatter.Metadata + if isAgentMode() { + meta = &formatter.Metadata{Command: "monitors get"} } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, meta) } func runMonitorsDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/root.go b/cmd/root.go index f6ed1773..5a213ad4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ package cmd import ( "bufio" + "encoding/json" "errors" "fmt" "io" @@ -15,8 +16,11 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadog" "github.com/DataDog/pup/internal/version" + "github.com/DataDog/pup/pkg/agenthelp" "github.com/DataDog/pup/pkg/client" "github.com/DataDog/pup/pkg/config" + "github.com/DataDog/pup/pkg/formatter" + "github.com/DataDog/pup/pkg/useragent" "github.com/spf13/cobra" ) @@ -33,6 +37,8 @@ var ( ddClient *client.Client outputFormat string autoApprove bool + agentFlag bool + hlpFlag bool // Dependency injection points for testing clientFactory = defaultClientFactory @@ -48,15 +54,96 @@ var rootCmd = &cobra.Command{ with Datadog APIs. It supports both API key and OAuth2 authentication.`, Version: version.Version, SilenceUsage: true, // Don't show usage on errors, only on --help or invalid args + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + handled, err := HandleHlpFlag(cmd) + if err != nil { + return err + } + if handled { + cmd.SilenceErrors = true + return errHlpHandled + } + return nil + }, + // RunE is needed so that 'pup --hlp' (no subcommand) invokes the PersistentPreRunE. + // Without RunE, cobra shows help text instead. + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, } +// errHlpHandled is a sentinel error returned after --hlp output to stop execution. +// Execute/ExecuteWithArgs checks for this and treats it as a success exit. +var errHlpHandled = errors.New("hlp handled") + // Execute adds all child commands to the root command and sets flags appropriately. func Execute() error { - return ExecuteWithArgs(os.Args[1:]) + return suppressHlpError(ExecuteWithArgs(os.Args[1:])) +} + +// suppressHlpError converts the --hlp sentinel error to nil so callers see success. +func suppressHlpError(err error) error { + if errors.Is(err, errHlpHandled) { + return nil + } + return err +} + +// hasFlag checks if a flag is present in the args. +func hasFlag(args []string, flag string) bool { + for _, a := range args { + if a == flag { + return true + } + } + return false +} + +// handleHlpArgs processes --hlp by finding the first non-flag arg as subtree name. +func handleHlpArgs(args []string) error { + var subtree string + for _, a := range args { + if a == "--hlp" { + continue + } + if !isFlag(a) { + subtree = a + break + } + } + return printHlpSchema(subtree) +} + +// printHlpSchema generates and prints the JSON schema for the given subtree. +func printHlpSchema(subtree string) error { + var data interface{} + if subtree != "" { + schema := agenthelp.GenerateSubtreeSchema(rootCmd, subtree) + if schema != nil { + data = schema + } + } + if data == nil { + s := agenthelp.GenerateSchema(rootCmd) + data = &s + } + + out, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal schema: %w", err) + } + printOutput("%s\n", string(out)) + return nil } // ExecuteWithArgs executes the root command with the given arguments func ExecuteWithArgs(args []string) error { + // Handle --hlp before cobra processes args, because group commands (e.g. "logs") + // have no RunE and cobra would show help text instead of invoking PersistentPreRunE. + if hasFlag(args, "--hlp") { + return handleHlpArgs(args) + } + // IMPORTANT: Aliases are checked LAST to prevent overriding built-in commands. // This ensures that no alias can shadow an existing pup command, even if validation // is bypassed or a new command is added that conflicts with an existing alias. @@ -72,13 +159,13 @@ func ExecuteWithArgs(args []string) error { // Expand the alias by replacing args[0] with the alias command expandedArgs := expandAlias(aliasCommand, args[1:]) rootCmd.SetArgs(expandedArgs) - return rootCmd.Execute() + return suppressHlpError(rootCmd.Execute()) } } // Not an alias or is a built-in command, execute normally rootCmd.SetArgs(args) - return rootCmd.Execute() + return suppressHlpError(rootCmd.Execute()) } // expandAlias expands an alias command and appends additional arguments @@ -168,6 +255,8 @@ func init() { // Global flags rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "json", "Output format (json, table, yaml)") rootCmd.PersistentFlags().BoolVarP(&autoApprove, "yes", "y", false, "Skip confirmation prompts (auto-approve all operations)") + rootCmd.PersistentFlags().BoolVar(&agentFlag, "agent", false, "Enable agent mode (auto-detected for AI coding assistants)") + rootCmd.PersistentFlags().BoolVar(&hlpFlag, "hlp", false, "Output complete command schema as JSON (for AI agents)") // Add subcommands rootCmd.AddCommand(versionCmd) @@ -212,6 +301,7 @@ func init() { rootCmd.AddCommand(productAnalyticsCmd) rootCmd.AddCommand(casesCmd) rootCmd.AddCommand(apmCmd) + rootCmd.AddCommand(agentCmd) } // initConfig reads in config file and ENV variables if set. @@ -230,6 +320,17 @@ func initConfig() { fmt.Fprintf(os.Stderr, "Warning: failed to set DD_CLI_AUTO_APPROVE: %v\n", err) } } + + // Determine agent mode: explicit flag takes precedence, then auto-detect + if agentFlag || useragent.IsAgentMode() { + cfg.AgentMode = true + cfg.AutoApprove = true + } +} + +// isAgentMode returns true if the current session is in agent mode. +func isAgentMode() bool { + return cfg != nil && cfg.AgentMode } // getClient returns a configured Datadog client @@ -279,6 +380,27 @@ func printOutput(format string, a ...any) { _, _ = fmt.Fprintf(outputWriter, format, a...) } +// formatAndPrint formats data with optional agent envelope and prints it. +// In agent mode, data is wrapped in an AgentEnvelope with metadata. +// In human mode, data is formatted using the standard formatter. +func formatAndPrint(data interface{}, meta *formatter.Metadata) error { + if isAgentMode() { + output, err := formatter.WrapForAgent(data, meta) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil + } + + output, err := formatter.FormatOutput(data, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + // readConfirmation reads user confirmation from input func readConfirmation() (string, error) { scanner := bufio.NewScanner(inputReader) @@ -308,6 +430,7 @@ func extractAPIErrorBody(err error) string { // formatAPIError creates user-friendly error messages for API errors. // It extracts the API response body from GenericOpenAPIError when available // and appends contextual guidance based on the HTTP status code. +// In agent mode, returns a structured JSON error. func formatAPIError(operation string, err error, response any) error { type httpResponse interface { StatusCode() int @@ -315,31 +438,35 @@ func formatAPIError(operation string, err error, response any) error { if r, ok := response.(httpResponse); ok && r != nil { statusCode := r.StatusCode() + apiBody := extractAPIErrorBody(err) + + // In agent mode, return structured JSON error + if isAgentMode() { + jsonErr, fmtErr := formatter.FormatAgentError(operation, statusCode, err.Error(), apiBody) + if fmtErr == nil { + return fmt.Errorf("%s", jsonErr) + } + } + baseMsg := fmt.Sprintf("failed to %s: %v (status: %d)", operation, err, statusCode) // Include API response body if available - if body := extractAPIErrorBody(err); body != "" { - baseMsg = fmt.Sprintf("failed to %s: %v (status: %d)\nAPI Response: %s", operation, err, statusCode, body) + if apiBody != "" { + baseMsg = fmt.Sprintf("failed to %s: %v (status: %d)\nAPI Response: %s", operation, err, statusCode, apiBody) } switch { case statusCode >= 500: - // 5xx Server errors return fmt.Errorf("%s\n\nThe Datadog API is experiencing issues. Please try again later or check https://status.datadoghq.com/", baseMsg) case statusCode == 429: - // Rate limiting return fmt.Errorf("%s\n\nYou are being rate limited. Please wait a moment and try again.", baseMsg) case statusCode == 403: - // Forbidden return fmt.Errorf("%s\n\nAccess denied. Verify your API/App keys have the required permissions.", baseMsg) case statusCode == 401: - // Unauthorized return fmt.Errorf("%s\n\nAuthentication failed. Run 'pup auth login' or verify your DD_API_KEY and DD_APP_KEY.", baseMsg) case statusCode == 404: - // Not found return fmt.Errorf("%s\n\nResource not found. Verify the ID or check if the resource was deleted.", baseMsg) case statusCode >= 400: - // Other 4xx client errors return fmt.Errorf("%s\n\nInvalid request. Check your parameters and try again.", baseMsg) default: return fmt.Errorf("%s", baseMsg) diff --git a/pkg/agenthelp/agenthelp.go b/pkg/agenthelp/agenthelp.go new file mode 100644 index 00000000..ad2fe553 --- /dev/null +++ b/pkg/agenthelp/agenthelp.go @@ -0,0 +1,231 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package agenthelp + +import ( + "github.com/DataDog/pup/internal/version" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Schema is the top-level structure returned by --hlp. +type Schema struct { + Version string `json:"version"` + Description string `json:"description"` + Auth AuthInfo `json:"auth"` + GlobalFlags []FlagInfo `json:"global_flags"` + Commands []CommandInfo `json:"commands"` + QuerySyntax map[string]string `json:"query_syntax"` + TimeFormats TimeFormats `json:"time_formats"` + Workflows []Workflow `json:"workflows"` + BestPractices []string `json:"best_practices"` + AntiPatterns []string `json:"anti_patterns"` +} + +// AuthInfo describes authentication options. +type AuthInfo struct { + OAuth string `json:"oauth"` + APIKeys string `json:"api_keys"` +} + +// FlagInfo describes a command flag. +type FlagInfo struct { + Name string `json:"name"` + Type string `json:"type"` + Default string `json:"default,omitempty"` + Description string `json:"description"` +} + +// CommandInfo describes a command or subcommand. +type CommandInfo struct { + Name string `json:"name"` + FullPath string `json:"full_path"` + Description string `json:"description"` + Flags []FlagInfo `json:"flags,omitempty"` + Examples []string `json:"examples,omitempty"` + ReadOnly bool `json:"read_only"` + Subcommands []CommandInfo `json:"subcommands,omitempty"` +} + +// TimeFormats describes supported time format options. +type TimeFormats struct { + Relative []string `json:"relative"` + Absolute []string `json:"absolute"` + Examples []string `json:"examples"` +} + +// Workflow describes a multi-step agent workflow. +type Workflow struct { + Name string `json:"name"` + Steps []string `json:"steps"` +} + +// GenerateSchema builds the complete schema from a cobra command tree. +func GenerateSchema(root *cobra.Command) Schema { + return Schema{ + Version: version.Version, + Description: "Pup - Datadog API CLI. Provides OAuth2 + API key authentication for querying metrics, logs, monitors, traces, and 30+ other Datadog API domains.", + Auth: AuthInfo{ + OAuth: "pup auth login", + APIKeys: "Set DD_API_KEY + DD_APP_KEY + DD_SITE environment variables", + }, + GlobalFlags: extractGlobalFlags(root), + Commands: extractCommands(root, ""), + QuerySyntax: GetQuerySyntax(), + TimeFormats: GetTimeFormats(), + Workflows: GetWorkflows(), + BestPractices: GetBestPractices(), + AntiPatterns: GetAntiPatterns(), + } +} + +// GenerateSubtreeSchema builds a schema for a specific subtree of commands. +func GenerateSubtreeSchema(root *cobra.Command, subtreeName string) *Schema { + for _, cmd := range root.Commands() { + if cmd.Name() == subtreeName { + schema := Schema{ + Version: version.Version, + Description: cmd.Short, + Auth: AuthInfo{ + OAuth: "pup auth login", + APIKeys: "Set DD_API_KEY + DD_APP_KEY + DD_SITE environment variables", + }, + GlobalFlags: extractGlobalFlags(root), + Commands: []CommandInfo{buildCommandInfo(cmd, subtreeName)}, + QuerySyntax: filterQuerySyntax(subtreeName), + TimeFormats: GetTimeFormats(), + BestPractices: GetBestPractices(), + AntiPatterns: GetAntiPatterns(), + } + return &schema + } + } + return nil +} + +// CompactSchema is a minimal schema with just command names and flags. +type CompactSchema struct { + Version string `json:"version"` + Commands []CompactCommand `json:"commands"` +} + +// CompactCommand is a minimal command representation. +type CompactCommand struct { + Name string `json:"name"` + Flags []string `json:"flags,omitempty"` + Subcommands []CompactCommand `json:"subcommands,omitempty"` +} + +// GenerateCompactSchema builds a token-efficient schema. +func GenerateCompactSchema(root *cobra.Command) CompactSchema { + return CompactSchema{ + Version: version.Version, + Commands: extractCompactCommands(root), + } +} + +func extractGlobalFlags(cmd *cobra.Command) []FlagInfo { + var flags []FlagInfo + cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { + flags = append(flags, FlagInfo{ + Name: "--" + f.Name, + Type: f.Value.Type(), + Default: f.DefValue, + Description: f.Usage, + }) + }) + return flags +} + +func extractCommands(parent *cobra.Command, prefix string) []CommandInfo { + var commands []CommandInfo + for _, cmd := range parent.Commands() { + if cmd.Hidden || cmd.Name() == "help" || cmd.Name() == "completion" { + continue + } + fullPath := cmd.Name() + if prefix != "" { + fullPath = prefix + " " + cmd.Name() + } + commands = append(commands, buildCommandInfo(cmd, fullPath)) + } + return commands +} + +func buildCommandInfo(cmd *cobra.Command, fullPath string) CommandInfo { + info := CommandInfo{ + Name: cmd.Name(), + FullPath: fullPath, + Description: cmd.Short, + ReadOnly: isReadOnlyCommand(cmd.Name()), + Flags: extractLocalFlags(cmd), + Examples: extractExamples(cmd), + } + + for _, sub := range cmd.Commands() { + if sub.Hidden || sub.Name() == "help" || sub.Name() == "completion" { + continue + } + subPath := fullPath + " " + sub.Name() + info.Subcommands = append(info.Subcommands, buildCommandInfo(sub, subPath)) + } + + return info +} + +func extractLocalFlags(cmd *cobra.Command) []FlagInfo { + var flags []FlagInfo + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + flags = append(flags, FlagInfo{ + Name: "--" + f.Name, + Type: f.Value.Type(), + Default: f.DefValue, + Description: f.Usage, + }) + }) + return flags +} + +func extractExamples(cmd *cobra.Command) []string { + if cmd.Example == "" { + return nil + } + return []string{cmd.Example} +} + +func extractCompactCommands(parent *cobra.Command) []CompactCommand { + var commands []CompactCommand + for _, cmd := range parent.Commands() { + if cmd.Hidden || cmd.Name() == "help" || cmd.Name() == "completion" { + continue + } + cc := CompactCommand{Name: cmd.Name()} + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + cc.Flags = append(cc.Flags, "--"+f.Name) + }) + cc.Subcommands = extractCompactCommands(cmd) + commands = append(commands, cc) + } + return commands +} + +// isReadOnlyCommand returns true for commands that only read data. +func isReadOnlyCommand(name string) bool { + writeCommands := map[string]bool{ + "delete": true, "create": true, "update": true, "set": true, + "import": true, "login": true, "logout": true, + } + return !writeCommands[name] +} + +// filterQuerySyntax returns query syntax relevant to a specific domain. +func filterQuerySyntax(domain string) map[string]string { + all := GetQuerySyntax() + if syntax, ok := all[domain]; ok { + return map[string]string{domain: syntax} + } + return all +} diff --git a/pkg/agenthelp/agenthelp_test.go b/pkg/agenthelp/agenthelp_test.go new file mode 100644 index 00000000..bccd3b36 --- /dev/null +++ b/pkg/agenthelp/agenthelp_test.go @@ -0,0 +1,204 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package agenthelp + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func newTestRoot() *cobra.Command { + root := &cobra.Command{Use: "pup", Short: "test CLI"} + root.PersistentFlags().String("output", "json", "Output format") + root.PersistentFlags().Bool("agent", false, "Agent mode") + + monitors := &cobra.Command{Use: "monitors", Short: "Manage monitors"} + monitorsList := &cobra.Command{Use: "list", Short: "List monitors"} + monitorsList.Flags().String("tags", "", "Filter by tags") + monitorsList.Flags().Int("limit", 200, "Maximum results") + monitorsGet := &cobra.Command{Use: "get", Short: "Get monitor details"} + monitorsDelete := &cobra.Command{Use: "delete", Short: "Delete a monitor"} + monitors.AddCommand(monitorsList, monitorsGet, monitorsDelete) + + logs := &cobra.Command{Use: "logs", Short: "Search and analyze logs"} + logsSearch := &cobra.Command{Use: "search", Short: "Search logs"} + logsSearch.Flags().String("query", "", "Search query") + logsSearch.Flags().String("from", "1h", "Start time") + logs.AddCommand(logsSearch) + + root.AddCommand(monitors, logs) + return root +} + +func TestGenerateSchema(t *testing.T) { + root := newTestRoot() + schema := GenerateSchema(root) + + if schema.Version == "" { + t.Error("Schema.Version should not be empty") + } + if schema.Description == "" { + t.Error("Schema.Description should not be empty") + } + if schema.Auth.OAuth == "" { + t.Error("Schema.Auth.OAuth should not be empty") + } + if len(schema.GlobalFlags) == 0 { + t.Error("Schema.GlobalFlags should not be empty") + } + if len(schema.Commands) == 0 { + t.Error("Schema.Commands should not be empty") + } + if len(schema.QuerySyntax) == 0 { + t.Error("Schema.QuerySyntax should not be empty") + } + if len(schema.TimeFormats.Relative) == 0 { + t.Error("Schema.TimeFormats.Relative should not be empty") + } + if len(schema.Workflows) == 0 { + t.Error("Schema.Workflows should not be empty") + } + if len(schema.BestPractices) == 0 { + t.Error("Schema.BestPractices should not be empty") + } + if len(schema.AntiPatterns) == 0 { + t.Error("Schema.AntiPatterns should not be empty") + } +} + +func TestGenerateSchema_CommandsIncludeSubcommands(t *testing.T) { + root := newTestRoot() + schema := GenerateSchema(root) + + var found bool + for _, cmd := range schema.Commands { + if cmd.Name == "monitors" { + found = true + if len(cmd.Subcommands) != 3 { + t.Errorf("monitors should have 3 subcommands, got %d", len(cmd.Subcommands)) + } + for _, sub := range cmd.Subcommands { + if sub.Name == "list" { + if len(sub.Flags) == 0 { + t.Error("monitors list should have flags") + } + } + } + } + } + if !found { + t.Error("monitors command not found in schema") + } +} + +func TestGenerateSchema_GlobalFlags(t *testing.T) { + root := newTestRoot() + schema := GenerateSchema(root) + + flagNames := make(map[string]bool) + for _, f := range schema.GlobalFlags { + flagNames[f.Name] = true + } + + if !flagNames["--output"] { + t.Error("Global flags should include --output") + } + if !flagNames["--agent"] { + t.Error("Global flags should include --agent") + } +} + +func TestGenerateSubtreeSchema(t *testing.T) { + root := newTestRoot() + + schema := GenerateSubtreeSchema(root, "monitors") + if schema == nil { + t.Fatal("GenerateSubtreeSchema should not return nil for 'monitors'") + } + if len(schema.Commands) != 1 { + t.Errorf("Subtree schema should have 1 command, got %d", len(schema.Commands)) + } + if schema.Commands[0].Name != "monitors" { + t.Errorf("Subtree command should be 'monitors', got %q", schema.Commands[0].Name) + } +} + +func TestGenerateSubtreeSchema_NotFound(t *testing.T) { + root := newTestRoot() + + schema := GenerateSubtreeSchema(root, "nonexistent") + if schema != nil { + t.Error("GenerateSubtreeSchema should return nil for nonexistent command") + } +} + +func TestGenerateCompactSchema(t *testing.T) { + root := newTestRoot() + compact := GenerateCompactSchema(root) + + if compact.Version == "" { + t.Error("CompactSchema.Version should not be empty") + } + if len(compact.Commands) == 0 { + t.Error("CompactSchema.Commands should not be empty") + } + + for _, cmd := range compact.Commands { + if cmd.Name == "monitors" { + if len(cmd.Subcommands) != 3 { + t.Errorf("monitors should have 3 subcommands, got %d", len(cmd.Subcommands)) + } + for _, sub := range cmd.Subcommands { + if sub.Name == "list" && len(sub.Flags) == 0 { + t.Error("monitors list should have flags in compact schema") + } + } + } + } +} + +func TestIsReadOnlyCommand(t *testing.T) { + tests := []struct { + name string + want bool + }{ + {"list", true}, + {"get", true}, + {"search", true}, + {"query", true}, + {"delete", false}, + {"create", false}, + {"update", false}, + {"set", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isReadOnlyCommand(tt.name) + if got != tt.want { + t.Errorf("isReadOnlyCommand(%q) = %v, want %v", tt.name, got, tt.want) + } + }) + } +} + +func TestFilterQuerySyntax(t *testing.T) { + // Known domain + result := filterQuerySyntax("logs") + if _, ok := result["logs"]; !ok { + t.Error("filterQuerySyntax('logs') should contain 'logs' key") + } + if len(result) != 1 { + t.Errorf("filterQuerySyntax('logs') should have 1 entry, got %d", len(result)) + } + + // Unknown domain returns all + result = filterQuerySyntax("unknown") + if len(result) < 2 { + t.Error("filterQuerySyntax('unknown') should return all entries") + } +} diff --git a/pkg/agenthelp/guide.go b/pkg/agenthelp/guide.go new file mode 100644 index 00000000..7a478898 --- /dev/null +++ b/pkg/agenthelp/guide.go @@ -0,0 +1,44 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package agenthelp + +import ( + _ "embed" + "strings" +) + +//go:embed guide.md +var guideContent string + +// GetGuide returns the full steering guide. +func GetGuide() string { + return guideContent +} + +// GetGuideSection returns a specific domain section from the guide. +// Returns the full guide if the domain is not found. +func GetGuideSection(domain string) string { + // Try capitalized version first (e.g., "## Logs") + capitalized := strings.ToUpper(domain[:1]) + domain[1:] + heading := "## " + capitalized + idx := strings.Index(guideContent, heading) + if idx == -1 { + // Try exact case + heading = "## " + domain + idx = strings.Index(guideContent, heading) + if idx == -1 { + return guideContent + } + } + + // Find the next ## heading + rest := guideContent[idx+len(heading):] + nextSection := strings.Index(rest, "\n## ") + if nextSection == -1 { + return guideContent[idx:] + } + return guideContent[idx : idx+len(heading)+nextSection] +} diff --git a/pkg/agenthelp/guide.md b/pkg/agenthelp/guide.md new file mode 100644 index 00000000..411b293d --- /dev/null +++ b/pkg/agenthelp/guide.md @@ -0,0 +1,318 @@ +# Pup Agent Guide + +Pup is a CLI for the Datadog API. This guide helps AI coding agents use pup effectively. + +## Quick Start + +```bash +# Authenticate (one-time setup) +pup auth login + +# Or use API keys +export DD_API_KEY="your-key" DD_APP_KEY="your-key" DD_SITE="datadoghq.com" + +# Get the full command schema (recommended first step) +pup --hlp + +# Get schema for a specific domain +pup logs --hlp +``` + +## Authentication + +- **OAuth2 (recommended):** `pup auth login` — opens browser for secure login +- **API keys:** Set `DD_API_KEY`, `DD_APP_KEY`, and `DD_SITE` environment variables +- OAuth2 tokens are stored in the OS keychain and refresh automatically +- Some endpoints (logs search) require API keys even with OAuth2 + +## Logs + +### Query Syntax +``` +status:error # Filter by status +service:web-app # Filter by service +@user.id:12345 # Filter by custom attribute +host:i-* # Wildcard matching +"exact error message" # Exact phrase matching +status:error AND service:web # Boolean AND +status:error OR status:warn # Boolean OR +NOT status:info # Negation +-status:info # Shorthand negation +``` + +### Commands +```bash +# Search logs (v1 API, follows pagination) +pup logs search --query="status:error" --from=1h --limit=100 + +# Query logs (v2 API) +pup logs query --query="service:api AND status:error" --from=4h + +# Aggregate logs (counts, distributions) +pup logs aggregate --query="*" --from=1h --compute="count" --group-by="service" +pup logs aggregate --query="service:api" --from=1h --compute="avg(@duration)" --group-by="@http.status_code" + +# List logs (v2 API, simple) +pup logs list --from=1h --limit=20 + +# Storage tiers: indexes (default), online-archives, flex +pup logs search --query="*" --from=30d --storage="flex" +``` + +### Tips +- Always specify `--from` for a time range +- Use `aggregate` for counting/statistics, not `search` + local processing +- Default limit is 50; increase with `--limit` up to 1000 +- Supported compute functions: count, avg, sum, min, max, cardinality, percentile + +## Metrics + +### Query Syntax +``` +:{} by {} + +# Examples: +avg:system.cpu.user{*} # All hosts, average CPU +avg:system.cpu.user{env:prod} by {host} # By host, production only +sum:trace.servlet.request.hits{service:web} # Request count +max:system.mem.used{*} by {host} # Max memory by host +``` + +### Commands +```bash +# Query metrics (timeseries) +pup metrics query --query="avg:system.cpu.user{env:prod} by {host}" --from=1h + +# List metric names +pup metrics list --query="system.cpu" + +# Get metric metadata +pup metrics metadata get "system.cpu.user" + +# Submit metrics +pup metrics submit --metric="custom.metric" --value=42 --tags="env:test" +``` + +### Tips +- Aggregations: avg, sum, min, max, count +- Always include `{...}` filter even if empty: `{*}` means all +- Time range defaults to 1h if not specified + +## Monitors + +### Commands +```bash +# List monitors with filtering +pup monitors list --tags="env:production" --limit=500 +pup monitors list --name="CPU" --tags="team:backend" + +# Search monitors (full-text) +pup monitors search --query="database" + +# Get monitor details +pup monitors get 12345678 + +# Delete monitor (prompts for confirmation; use --yes to skip) +pup monitors delete 12345678 --yes +``` + +### Tips +- Use `--tags` for efficient filtering at the API level +- Default limit is 200; max is 1000 +- Search supports full-text across monitor names and queries +- Monitor states: OK, Alert, Warn, No Data + +## APM / Traces + +### Query Syntax +``` +service: # Filter by service +resource_name: # Filter by resource/endpoint +@duration:>5000000000 # Duration > 5 seconds (NANOSECONDS!) +status:error # Error traces only +operation_name:rack.request # Filter by operation +env:production # Filter by environment +``` + +**CRITICAL: APM durations are in NANOSECONDS** +- 1 millisecond = 1,000,000 ns +- 1 second = 1,000,000,000 ns +- 5 seconds = 5,000,000,000 ns + +### Commands +```bash +# List APM services +pup apm services list + +# Search traces +pup traces search --query="service:web-api AND @duration:>1000000000" --from=1h + +# List spans +pup traces list --query="service:web-api" --from=1h --limit=50 +``` + +## RUM (Real User Monitoring) + +### Query Syntax +``` +@type:error # Error events +@session.type:user # User sessions (not synthetic) +@view.url_path:/checkout # Specific page +@action.type:click # Click actions +service: # Filter by application +``` + +### Commands +```bash +# List RUM applications +pup rum apps list + +# Search RUM events +pup rum events search --query="@type:error" --from=1h + +# Aggregate RUM data +pup rum aggregate --query="@type:view" --from=4h --compute="avg(@view.loading_time)" +``` + +## Incidents + +```bash +# List active incidents +pup incidents list --query="status:active" + +# Get incident details +pup incidents get + +# List incident timeline +pup incidents timeline +``` + +## SLOs + +```bash +# List all SLOs +pup slos list + +# Get SLO details +pup slos get + +# Get SLO history +pup slos history --from=7d +``` + +## Security + +```bash +# List security rules +pup security rules list + +# Search security signals +pup security signals list --query="status:critical" --from=1d + +# List security filters +pup security filters list +``` + +## Dashboards + +```bash +# List dashboards +pup dashboards list + +# Get dashboard details (includes all widgets and queries) +pup dashboards get +``` + +## Events + +```bash +# List events +pup events list --from=1h + +# Search events by source +pup events search --query="sources:pagerduty" --from=1d +``` + +## Common Patterns + +### Error Investigation +```bash +# 1. Check for errors in logs +pup logs aggregate --query="status:error" --from=1h --compute="count" --group-by="service" + +# 2. Drill into the affected service +pup logs search --query="status:error AND service:" --from=1h --limit=20 + +# 3. Check monitors for that service +pup monitors list --tags="service:" + +# 4. Check recent deployments/events +pup events list --from=4h +``` + +### Performance Investigation +```bash +# 1. Check service latency +pup metrics query --query="avg:trace.servlet.request.duration{service:} by {resource_name}" --from=1h + +# 2. Find slow traces (>5 seconds) +pup traces search --query="service: AND @duration:>5000000000" --from=1h + +# 3. Check resource utilization +pup metrics query --query="avg:system.cpu.user{service:} by {host}" --from=1h +``` + +## Time Ranges + +All time-related flags (`--from`, `--to`) accept: + +| Format | Example | Description | +|--------|---------|-------------| +| Relative short | `1h`, `30m`, `7d`, `5s` | Ago from now | +| Relative long | `5min`, `2hours`, `3days` | Ago from now | +| With spaces | `"5 minutes"`, `"2 hours"` | Ago from now | +| RFC3339 | `2024-01-01T00:00:00Z` | Absolute time | +| Unix ms | `1704067200000` | Milliseconds since epoch | +| Keyword | `now` | Current time | + +## Output Formats + +```bash +# JSON (default, recommended for agents) +pup monitors list --output=json + +# Table (human-readable) +pup monitors list --output=table + +# YAML +pup monitors list --output=yaml +``` + +## Error Handling + +| Status | Meaning | Action | +|--------|---------|--------| +| 401 | Authentication failed | Run `pup auth login` or check API keys | +| 403 | Insufficient permissions | Verify API/App key permissions | +| 404 | Resource not found | Check the ID or resource name | +| 429 | Rate limited | Wait and retry with backoff | +| 5xx | Server error | Retry after a short delay | + +## Agent Mode + +Agent mode is auto-detected when running inside AI coding assistants (Claude Code, Cursor, Codex, etc.) or can be enabled explicitly: + +```bash +# Explicit flag +pup --agent monitors list + +# Environment variable +DD_AGENT_MODE=1 pup monitors list + +# Auto-detected from: CLAUDECODE, CLAUDE_CODE, CURSOR_AGENT, CODEX, AIDER, etc. +``` + +In agent mode: +- Confirmation prompts are auto-approved (no stdin hangs) +- Output is JSON by default +- Structured error responses with suggestions diff --git a/pkg/agenthelp/guide_test.go b/pkg/agenthelp/guide_test.go new file mode 100644 index 00000000..3d6e938c --- /dev/null +++ b/pkg/agenthelp/guide_test.go @@ -0,0 +1,59 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package agenthelp + +import ( + "strings" + "testing" +) + +func TestGetGuide(t *testing.T) { + guide := GetGuide() + if guide == "" { + t.Fatal("GetGuide() should not be empty") + } + if !strings.Contains(guide, "# Pup Agent Guide") { + t.Error("Guide should contain the title") + } + if !strings.Contains(guide, "## Logs") { + t.Error("Guide should contain Logs section") + } + if !strings.Contains(guide, "## Metrics") { + t.Error("Guide should contain Metrics section") + } + if !strings.Contains(guide, "## Monitors") { + t.Error("Guide should contain Monitors section") + } +} + +func TestGetGuideSection(t *testing.T) { + tests := []struct { + domain string + shouldMatch string + }{ + {"logs", "## Logs"}, + {"metrics", "## Metrics"}, + {"monitors", "## Monitors"}, + {"Logs", "## Logs"}, + } + + for _, tt := range tests { + t.Run(tt.domain, func(t *testing.T) { + section := GetGuideSection(tt.domain) + if !strings.Contains(section, tt.shouldMatch) { + t.Errorf("GetGuideSection(%q) should contain %q", tt.domain, tt.shouldMatch) + } + }) + } +} + +func TestGetGuideSection_NotFound(t *testing.T) { + section := GetGuideSection("nonexistent_domain_xyz") + guide := GetGuide() + if section != guide { + t.Error("GetGuideSection for unknown domain should return the full guide") + } +} diff --git a/pkg/agenthelp/steering.go b/pkg/agenthelp/steering.go new file mode 100644 index 00000000..ea320b8b --- /dev/null +++ b/pkg/agenthelp/steering.go @@ -0,0 +1,112 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package agenthelp + +// GetQuerySyntax returns query syntax documentation for each domain. +func GetQuerySyntax() map[string]string { + return map[string]string{ + "logs": `status:error, service:web-app, @attr:val, host:i-*, "exact phrase", AND/OR/NOT operators, -status:info (negation), wildcards with *`, + "metrics": `:{} by {}. Example: avg:system.cpu.user{env:prod} by {host}. Aggregations: avg, sum, min, max, count`, + "monitors": `Use --name for substring search, --tags for tag filtering (comma-separated). Search via --query for full-text search`, + "apm": `service: resource_name: @duration:>5000000000 (nanoseconds!) status:error operation_name:. Duration is always in nanoseconds`, + "rum": `@type:error @session.type:user @view.url_path:/checkout @action.type:click service:`, + "security": `@workflow.rule.type:log_detection source:cloudtrail @network.client.ip:10.0.0.0/8 status:critical`, + "events": `sources:nagios,pagerduty status:error priority:normal tags:env:prod`, + "traces": `service: resource_name: @duration:>5s (shorthand) env:production`, + } +} + +// GetTimeFormats returns documentation for supported time formats. +func GetTimeFormats() TimeFormats { + return TimeFormats{ + Relative: []string{"5s", "30m", "1h", "4h", "1d", "7d", "1w", "30d", "5min", "2hours", "3days"}, + Absolute: []string{"Unix timestamp in milliseconds", "RFC3339 (2024-01-01T00:00:00Z)"}, + Examples: []string{ + `--from=1h (1 hour ago)`, + `--from=30m --to=now`, + `--from=7d --to=1d (7 days ago to 1 day ago)`, + `--from=2024-01-01T00:00:00Z --to=2024-01-02T00:00:00Z`, + `--from="5 minutes"`, + }, + } +} + +// GetWorkflows returns common multi-step workflows for agents. +func GetWorkflows() []Workflow { + return []Workflow{ + { + Name: "Investigate errors", + Steps: []string{ + `pup logs search --query="status:error" --from=1h --limit=20`, + `pup logs aggregate --query="status:error" --from=1h --compute="count" --group-by="service"`, + `pup monitors list --tags="env:production" --limit=50`, + }, + }, + { + Name: "Performance investigation", + Steps: []string{ + `pup metrics query --query="avg:trace.servlet.request.duration{env:prod} by {service}" --from=1h`, + `pup logs search --query="@duration:>5000000000" --from=1h --limit=20`, + `pup apm services list`, + }, + }, + { + Name: "Monitor status check", + Steps: []string{ + `pup monitors list --tags="env:production" --limit=500`, + `pup monitors search --query="status:Alert"`, + `pup monitors get `, + }, + }, + { + Name: "Security audit", + Steps: []string{ + `pup audit-logs search --query="*" --from=1d --limit=100`, + `pup security rules list`, + `pup security signals list --query="status:critical" --from=1d`, + }, + }, + { + Name: "Service health overview", + Steps: []string{ + `pup slos list`, + `pup monitors list --tags="team:"`, + `pup incidents list --query="status:active"`, + }, + }, + } +} + +// GetBestPractices returns agent-specific best practices. +func GetBestPractices() []string { + return []string{ + "Always specify --from to set a time range; most commands default to 1h but be explicit", + "Start with narrow time ranges (1h) then widen if needed; large ranges are slow and expensive", + "Filter by service first when investigating issues: --query='service:'", + "Use --limit to control result size; default varies by command (50-200)", + "For monitors, use --tags to filter rather than listing all and parsing locally", + "APM durations are in NANOSECONDS: 1 second = 1000000000, 5ms = 5000000", + "Use 'pup logs aggregate' for counts and distributions instead of fetching all logs and counting locally", + "Prefer JSON output (default) for structured parsing; use --output=table only for human display", + "Chain narrow queries: first aggregate to find patterns, then search for specific examples", + "Use 'pup monitors search' for full-text search, 'pup monitors list' for tag/name filtering", + } +} + +// GetAntiPatterns returns common mistakes agents should avoid. +func GetAntiPatterns() []string { + return []string{ + "Don't omit --from on time-series queries; you'll get unexpected time ranges or errors", + "Don't use --limit=1000 as a first step; start with small limits and refine queries", + "Don't list all monitors/logs without filters in large organizations (>10k monitors)", + "Don't assume APM durations are in seconds or milliseconds; they are in NANOSECONDS", + "Don't fetch raw logs to count them; use 'pup logs aggregate --compute=count' instead", + "Don't use --from=30d unless you specifically need a month of data; it's slow", + "Don't retry failed requests without checking the error; 401 means re-authenticate, 403 means missing permissions", + "Don't use 'pup metrics query' without specifying an aggregation (avg, sum, max, min, count)", + "Don't pipe large JSON responses through multiple jq transforms; use query filters at the API level", + } +} diff --git a/pkg/agenthelp/steering_test.go b/pkg/agenthelp/steering_test.go new file mode 100644 index 00000000..ea869680 --- /dev/null +++ b/pkg/agenthelp/steering_test.go @@ -0,0 +1,74 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package agenthelp + +import "testing" + +func TestGetQuerySyntax(t *testing.T) { + syntax := GetQuerySyntax() + if len(syntax) == 0 { + t.Fatal("GetQuerySyntax() should not be empty") + } + + expectedDomains := []string{"logs", "metrics", "monitors", "apm", "rum", "security", "events", "traces"} + for _, domain := range expectedDomains { + if _, ok := syntax[domain]; !ok { + t.Errorf("GetQuerySyntax() missing domain %q", domain) + } + } +} + +func TestGetTimeFormats(t *testing.T) { + tf := GetTimeFormats() + if len(tf.Relative) == 0 { + t.Error("TimeFormats.Relative should not be empty") + } + if len(tf.Absolute) == 0 { + t.Error("TimeFormats.Absolute should not be empty") + } + if len(tf.Examples) == 0 { + t.Error("TimeFormats.Examples should not be empty") + } +} + +func TestGetWorkflows(t *testing.T) { + workflows := GetWorkflows() + if len(workflows) == 0 { + t.Fatal("GetWorkflows() should not be empty") + } + for _, w := range workflows { + if w.Name == "" { + t.Error("Workflow name should not be empty") + } + if len(w.Steps) == 0 { + t.Errorf("Workflow %q should have steps", w.Name) + } + } +} + +func TestGetBestPractices(t *testing.T) { + bp := GetBestPractices() + if len(bp) == 0 { + t.Fatal("GetBestPractices() should not be empty") + } + for _, p := range bp { + if p == "" { + t.Error("Best practice should not be empty string") + } + } +} + +func TestGetAntiPatterns(t *testing.T) { + ap := GetAntiPatterns() + if len(ap) == 0 { + t.Fatal("GetAntiPatterns() should not be empty") + } + for _, p := range ap { + if p == "" { + t.Error("Anti-pattern should not be empty string") + } + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index ba6c212a..404b2ccd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,6 +16,7 @@ type Config struct { AppKey string Site string AutoApprove bool + AgentMode bool } // Load loads configuration from environment variables diff --git a/pkg/formatter/envelope.go b/pkg/formatter/envelope.go new file mode 100644 index 00000000..8ddc657e --- /dev/null +++ b/pkg/formatter/envelope.go @@ -0,0 +1,82 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package formatter + +import "encoding/json" + +// AgentEnvelope wraps API responses with metadata for agent consumption. +type AgentEnvelope struct { + Status string `json:"status"` + Data interface{} `json:"data"` + Metadata *Metadata `json:"metadata,omitempty"` +} + +// Metadata contains structured information about the response. +type Metadata struct { + Count *int `json:"count,omitempty"` + Truncated bool `json:"truncated,omitempty"` + NextAction string `json:"next_action,omitempty"` + Command string `json:"command"` + Warnings []string `json:"warnings,omitempty"` +} + +// AgentError is a structured error response for agent mode. +type AgentError struct { + Status string `json:"status"` + ErrorCode int `json:"error_code,omitempty"` + Message string `json:"error_message"` + Operation string `json:"operation"` + Suggestions []string `json:"suggestions,omitempty"` + APIResponse string `json:"api_response,omitempty"` +} + +// WrapForAgent wraps data in an AgentEnvelope and formats as JSON. +func WrapForAgent(data interface{}, meta *Metadata) (string, error) { + env := AgentEnvelope{ + Status: "success", + Data: data, + Metadata: meta, + } + bytes, err := json.MarshalIndent(env, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FormatAgentError formats a structured error for agent mode. +func FormatAgentError(operation string, statusCode int, errMsg string, apiResponse string) (string, error) { + ae := AgentError{ + Status: "error", + ErrorCode: statusCode, + Message: errMsg, + Operation: operation, + Suggestions: suggestionsForStatus(statusCode), + APIResponse: apiResponse, + } + bytes, err := json.MarshalIndent(ae, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} + +func suggestionsForStatus(code int) []string { + switch { + case code == 401: + return []string{"Run 'pup auth login'", "Set DD_API_KEY and DD_APP_KEY"} + case code == 403: + return []string{"Verify your API/App keys have required permissions"} + case code == 404: + return []string{"Verify the resource ID", "Check if the resource was deleted"} + case code == 429: + return []string{"Wait and retry with backoff"} + case code >= 500: + return []string{"Retry after a short delay", "Check https://status.datadoghq.com/"} + default: + return nil + } +} diff --git a/pkg/formatter/envelope_test.go b/pkg/formatter/envelope_test.go new file mode 100644 index 00000000..f17362ba --- /dev/null +++ b/pkg/formatter/envelope_test.go @@ -0,0 +1,150 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package formatter + +import ( + "encoding/json" + "testing" +) + +func TestWrapForAgent(t *testing.T) { + data := map[string]string{"key": "value"} + count := 1 + meta := &Metadata{ + Count: &count, + Command: "test command", + } + + result, err := WrapForAgent(data, meta) + if err != nil { + t.Fatalf("WrapForAgent() error = %v", err) + } + + var envelope AgentEnvelope + if err := json.Unmarshal([]byte(result), &envelope); err != nil { + t.Fatalf("WrapForAgent() produced invalid JSON: %v", err) + } + + if envelope.Status != "success" { + t.Errorf("envelope.Status = %q, want %q", envelope.Status, "success") + } + if envelope.Metadata == nil { + t.Fatal("envelope.Metadata should not be nil") + } + if *envelope.Metadata.Count != 1 { + t.Errorf("envelope.Metadata.Count = %d, want 1", *envelope.Metadata.Count) + } + if envelope.Metadata.Command != "test command" { + t.Errorf("envelope.Metadata.Command = %q, want %q", envelope.Metadata.Command, "test command") + } +} + +func TestWrapForAgent_NilMetadata(t *testing.T) { + data := []int{1, 2, 3} + + result, err := WrapForAgent(data, nil) + if err != nil { + t.Fatalf("WrapForAgent() error = %v", err) + } + + var envelope AgentEnvelope + if err := json.Unmarshal([]byte(result), &envelope); err != nil { + t.Fatalf("WrapForAgent() produced invalid JSON: %v", err) + } + + if envelope.Status != "success" { + t.Errorf("envelope.Status = %q, want %q", envelope.Status, "success") + } + if envelope.Metadata != nil { + t.Error("envelope.Metadata should be nil when no metadata provided") + } +} + +func TestWrapForAgent_Truncated(t *testing.T) { + data := "some data" + count := 100 + meta := &Metadata{ + Count: &count, + Truncated: true, + NextAction: "Use --limit=500", + Command: "monitors list", + } + + result, err := WrapForAgent(data, meta) + if err != nil { + t.Fatalf("WrapForAgent() error = %v", err) + } + + var envelope AgentEnvelope + if err := json.Unmarshal([]byte(result), &envelope); err != nil { + t.Fatalf("WrapForAgent() produced invalid JSON: %v", err) + } + + if !envelope.Metadata.Truncated { + t.Error("envelope.Metadata.Truncated should be true") + } + if envelope.Metadata.NextAction != "Use --limit=500" { + t.Errorf("envelope.Metadata.NextAction = %q, want %q", envelope.Metadata.NextAction, "Use --limit=500") + } +} + +func TestFormatAgentError(t *testing.T) { + result, err := FormatAgentError("list monitors", 401, "authentication failed", "unauthorized") + if err != nil { + t.Fatalf("FormatAgentError() error = %v", err) + } + + var agentErr AgentError + if err := json.Unmarshal([]byte(result), &agentErr); err != nil { + t.Fatalf("FormatAgentError() produced invalid JSON: %v", err) + } + + if agentErr.Status != "error" { + t.Errorf("agentErr.Status = %q, want %q", agentErr.Status, "error") + } + if agentErr.ErrorCode != 401 { + t.Errorf("agentErr.ErrorCode = %d, want 401", agentErr.ErrorCode) + } + if agentErr.Operation != "list monitors" { + t.Errorf("agentErr.Operation = %q, want %q", agentErr.Operation, "list monitors") + } + if len(agentErr.Suggestions) == 0 { + t.Error("401 error should have suggestions") + } +} + +func TestFormatAgentError_AllStatusCodes(t *testing.T) { + tests := []struct { + code int + wantSuggestions bool + }{ + {401, true}, + {403, true}, + {404, true}, + {429, true}, + {500, true}, + {200, false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result, err := FormatAgentError("test", tt.code, "error", "") + if err != nil { + t.Fatalf("FormatAgentError() error = %v", err) + } + + var agentErr AgentError + if err := json.Unmarshal([]byte(result), &agentErr); err != nil { + t.Fatalf("FormatAgentError() produced invalid JSON: %v", err) + } + + hasSuggestions := len(agentErr.Suggestions) > 0 + if hasSuggestions != tt.wantSuggestions { + t.Errorf("status %d: hasSuggestions = %v, want %v", tt.code, hasSuggestions, tt.wantSuggestions) + } + }) + } +} diff --git a/pkg/useragent/useragent.go b/pkg/useragent/useragent.go index bae1fb52..29acc240 100644 --- a/pkg/useragent/useragent.go +++ b/pkg/useragent/useragent.go @@ -14,6 +14,34 @@ import ( "github.com/DataDog/pup/internal/version" ) +// AgentInfo contains information about a detected AI coding agent. +type AgentInfo struct { + Name string + Detected bool +} + +// agentDetector defines how to detect a specific AI coding agent. +type agentDetector struct { + Name string + EnvVars []string +} + +// agentDetectors is a table-driven registry of known AI coding agents. +// Order matters: first match wins when multiple agents are detected. +var agentDetectors = []agentDetector{ + {Name: "claude-code", EnvVars: []string{"CLAUDECODE", "CLAUDE_CODE"}}, + {Name: "cursor", EnvVars: []string{"CURSOR_AGENT"}}, + {Name: "codex", EnvVars: []string{"CODEX", "OPENAI_CODEX"}}, + {Name: "opencode", EnvVars: []string{"OPENCODE"}}, + {Name: "aider", EnvVars: []string{"AIDER"}}, + {Name: "cline", EnvVars: []string{"CLINE"}}, + {Name: "windsurf", EnvVars: []string{"WINDSURF_AGENT"}}, + {Name: "github-copilot", EnvVars: []string{"GITHUB_COPILOT"}}, + {Name: "amazon-q", EnvVars: []string{"AMAZON_Q", "AWS_Q_DEVELOPER"}}, + {Name: "gemini-code", EnvVars: []string{"GEMINI_CODE_ASSIST"}}, + {Name: "sourcegraph-cody", EnvVars: []string{"SRC_CODY"}}, +} + // Get returns the user agent string for pup CLI with optional AI agent detection. // // Format without agent: @@ -23,12 +51,6 @@ import ( // Format with agent: // // pup/v0.1.0 (go go1.25.0; os darwin; arch arm64; ai-agent claude-code) -// -// AI agents are detected via environment variables: -// - CLAUDECODE=1 or CLAUDE_CODE=1 → adds "ai-agent claude-code" -// - CURSOR_AGENT=true or CURSOR_AGENT=1 → adds "ai-agent cursor" -// -// If multiple agents are detected, CLAUDECODE takes precedence. func Get() string { base := fmt.Sprintf( "pup/%s (go %s; os %s; arch %s", @@ -44,19 +66,38 @@ func Get() string { return base + ")" } -// detectAgent detects AI coding assistant from environment variables. -// Returns empty string if no agent is detected. -func detectAgent() string { - // Check Claude Code (CLAUDECODE or CLAUDE_CODE) - if os.Getenv("CLAUDECODE") == "1" || os.Getenv("CLAUDE_CODE") == "1" { - return "claude-code" +// IsAgentMode returns true if any AI agent is detected or DD_AGENT_MODE=1 is set. +func IsAgentMode() bool { + if isEnvTruthy("DD_AGENT_MODE") { + return true } + return detectAgent() != "" +} - // Check Cursor (CURSOR_AGENT=true or CURSOR_AGENT=1) - cursorAgent := strings.ToLower(os.Getenv("CURSOR_AGENT")) - if cursorAgent == "true" || cursorAgent == "1" { - return "cursor" +// DetectAgentInfo returns information about the detected AI coding agent. +func DetectAgentInfo() AgentInfo { + agent := detectAgent() + if agent == "" { + return AgentInfo{} } + return AgentInfo{Name: agent, Detected: true} +} +// detectAgent detects AI coding assistant from environment variables. +// Returns empty string if no agent is detected. +func detectAgent() string { + for _, d := range agentDetectors { + for _, envVar := range d.EnvVars { + if isEnvTruthy(envVar) { + return d.Name + } + } + } return "" } + +// isEnvTruthy checks if an environment variable is set to a truthy value. +func isEnvTruthy(key string) bool { + val := strings.ToLower(os.Getenv(key)) + return val == "1" || val == "true" +} diff --git a/pkg/useragent/useragent_test.go b/pkg/useragent/useragent_test.go index 04042cc3..87fe3bd9 100644 --- a/pkg/useragent/useragent_test.go +++ b/pkg/useragent/useragent_test.go @@ -14,96 +14,74 @@ import ( "github.com/DataDog/pup/internal/version" ) +// allAgentEnvVars returns all env vars used by agent detectors plus DD_AGENT_MODE. +func allAgentEnvVars() []string { + vars := []string{"DD_AGENT_MODE"} + for _, d := range agentDetectors { + vars = append(vars, d.EnvVars...) + } + return vars +} + +// clearAllAgentEnvVars unsets every agent-related env var. +func clearAllAgentEnvVars() { + for _, v := range allAgentEnvVars() { + os.Unsetenv(v) + } +} + func TestGet_NoAgent(t *testing.T) { - // NOTE: Not parallel - modifies env vars - // Clear all agent environment variables - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") + clearAllAgentEnvVars() result := Get() - // Check format matches: pup/VERSION (go GOVERSION; os OS; arch ARCH) expected := "pup/" + version.Version + " (go " + runtime.Version() + "; os " + runtime.GOOS + "; arch " + runtime.GOARCH + ")" if result != expected { t.Errorf("Get() = %q, want %q", result, expected) } - // Verify no agent in output if strings.Contains(result, "ai-agent") { t.Errorf("Get() should not contain ai-agent, got %q", result) } } -func TestGet_WithClaudeCode(t *testing.T) { - // NOTE: Not parallel - modifies env vars +func TestGet_AllAgents(t *testing.T) { tests := []struct { - name string - envVar string - envVal string - wantSuf string + name string + envVar string + envVal string + wantName string }{ {"CLAUDECODE=1", "CLAUDECODE", "1", "claude-code"}, {"CLAUDE_CODE=1", "CLAUDE_CODE", "1", "claude-code"}, + {"CURSOR_AGENT=true", "CURSOR_AGENT", "true", "cursor"}, + {"CURSOR_AGENT=1", "CURSOR_AGENT", "1", "cursor"}, + {"CODEX=1", "CODEX", "1", "codex"}, + {"OPENAI_CODEX=1", "OPENAI_CODEX", "1", "codex"}, + {"OPENCODE=1", "OPENCODE", "1", "opencode"}, + {"AIDER=1", "AIDER", "1", "aider"}, + {"CLINE=1", "CLINE", "1", "cline"}, + {"WINDSURF_AGENT=1", "WINDSURF_AGENT", "1", "windsurf"}, + {"GITHUB_COPILOT=1", "GITHUB_COPILOT", "1", "github-copilot"}, + {"AMAZON_Q=1", "AMAZON_Q", "1", "amazon-q"}, + {"AWS_Q_DEVELOPER=true", "AWS_Q_DEVELOPER", "true", "amazon-q"}, + {"GEMINI_CODE_ASSIST=1", "GEMINI_CODE_ASSIST", "1", "gemini-code"}, + {"SRC_CODY=1", "SRC_CODY", "1", "sourcegraph-cody"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Clear all agent env vars first - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - - // Set test env var + clearAllAgentEnvVars() os.Setenv(tt.envVar, tt.envVal) defer os.Unsetenv(tt.envVar) result := Get() - // Verify agent is present in structured format - expectedAgent := "; ai-agent " + tt.wantSuf + ")" - if !strings.HasSuffix(result, expectedAgent) { - t.Errorf("Get() = %q, want suffix %q", result, expectedAgent) - } - - // Verify base format is still correct - expectedBase := "pup/" + version.Version + " (go " + runtime.Version() + "; os " + runtime.GOOS + "; arch " + runtime.GOARCH - if !strings.HasPrefix(result, expectedBase) { - t.Errorf("Get() = %q, want prefix %q", result, expectedBase) - } - }) - } -} - -func TestGet_WithCursor(t *testing.T) { - tests := []struct { - name string - envVal string - }{ - {"CURSOR_AGENT=true", "true"}, - {"CURSOR_AGENT=TRUE", "TRUE"}, - {"CURSOR_AGENT=1", "1"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Clear all agent env vars first - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - - // Set test env var - os.Setenv("CURSOR_AGENT", tt.envVal) - defer os.Unsetenv("CURSOR_AGENT") - - result := Get() - - // Verify agent is present in structured format - if !strings.HasSuffix(result, "; ai-agent cursor)") { - t.Errorf("Get() = %q, want suffix '; ai-agent cursor)'", result) + expectedSuffix := "; ai-agent " + tt.wantName + ")" + if !strings.HasSuffix(result, expectedSuffix) { + t.Errorf("Get() = %q, want suffix %q", result, expectedSuffix) } - // Verify base format is still correct expectedBase := "pup/" + version.Version + " (go " + runtime.Version() + "; os " + runtime.GOOS + "; arch " + runtime.GOARCH if !strings.HasPrefix(result, expectedBase) { t.Errorf("Get() = %q, want prefix %q", result, expectedBase) @@ -113,11 +91,7 @@ func TestGet_WithCursor(t *testing.T) { } func TestGet_WithMultipleAgents(t *testing.T) { - // Test precedence: CLAUDECODE should win when both are set - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - + clearAllAgentEnvVars() os.Setenv("CLAUDECODE", "1") os.Setenv("CURSOR_AGENT", "true") defer func() { @@ -127,7 +101,6 @@ func TestGet_WithMultipleAgents(t *testing.T) { result := Get() - // Should have ai-agent claude-code, not cursor if !strings.HasSuffix(result, "; ai-agent claude-code)") { t.Errorf("Get() = %q, want suffix '; ai-agent claude-code)' (CLAUDECODE should take precedence)", result) } @@ -139,106 +112,32 @@ func TestGet_WithMultipleAgents(t *testing.T) { func TestDetectAgent(t *testing.T) { tests := []struct { name string - setup func() - teardown func() + envVar string + envVal string want string }{ - { - name: "no agent", - setup: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - }, - teardown: func() {}, - want: "", - }, - { - name: "CLAUDECODE=1", - setup: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - os.Setenv("CLAUDECODE", "1") - }, - teardown: func() { - os.Unsetenv("CLAUDECODE") - }, - want: "claude-code", - }, - { - name: "CLAUDE_CODE=1", - setup: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - os.Setenv("CLAUDE_CODE", "1") - }, - teardown: func() { - os.Unsetenv("CLAUDE_CODE") - }, - want: "claude-code", - }, - { - name: "CURSOR_AGENT=true", - setup: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - os.Setenv("CURSOR_AGENT", "true") - }, - teardown: func() { - os.Unsetenv("CURSOR_AGENT") - }, - want: "cursor", - }, - { - name: "CURSOR_AGENT=1", - setup: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - os.Setenv("CURSOR_AGENT", "1") - }, - teardown: func() { - os.Unsetenv("CURSOR_AGENT") - }, - want: "cursor", - }, - { - name: "CURSOR_AGENT=false (invalid, should not detect)", - setup: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - os.Setenv("CURSOR_AGENT", "false") - }, - teardown: func() { - os.Unsetenv("CURSOR_AGENT") - }, - want: "", - }, - { - name: "multiple agents (CLAUDECODE precedence)", - setup: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") - os.Setenv("CLAUDECODE", "1") - os.Setenv("CURSOR_AGENT", "true") - }, - teardown: func() { - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CURSOR_AGENT") - }, - want: "claude-code", - }, + {"no agent", "", "", ""}, + {"CLAUDECODE=1", "CLAUDECODE", "1", "claude-code"}, + {"CLAUDE_CODE=1", "CLAUDE_CODE", "1", "claude-code"}, + {"CURSOR_AGENT=true", "CURSOR_AGENT", "true", "cursor"}, + {"CURSOR_AGENT=1", "CURSOR_AGENT", "1", "cursor"}, + {"CURSOR_AGENT=false (not truthy)", "CURSOR_AGENT", "false", ""}, + {"CODEX=1", "CODEX", "1", "codex"}, + {"AIDER=true", "AIDER", "true", "aider"}, + {"WINDSURF_AGENT=1", "WINDSURF_AGENT", "1", "windsurf"}, + {"GITHUB_COPILOT=1", "GITHUB_COPILOT", "1", "github-copilot"}, + {"AMAZON_Q=1", "AMAZON_Q", "1", "amazon-q"}, + {"GEMINI_CODE_ASSIST=1", "GEMINI_CODE_ASSIST", "1", "gemini-code"}, + {"SRC_CODY=1", "SRC_CODY", "1", "sourcegraph-cody"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.setup() - defer tt.teardown() + clearAllAgentEnvVars() + if tt.envVar != "" { + os.Setenv(tt.envVar, tt.envVal) + defer os.Unsetenv(tt.envVar) + } got := detectAgent() if got != tt.want { @@ -248,51 +147,126 @@ func TestDetectAgent(t *testing.T) { } } +func TestIsAgentMode(t *testing.T) { + tests := []struct { + name string + envVar string + envVal string + want bool + }{ + {"no env", "", "", false}, + {"DD_AGENT_MODE=1", "DD_AGENT_MODE", "1", true}, + {"DD_AGENT_MODE=true", "DD_AGENT_MODE", "true", true}, + {"DD_AGENT_MODE=false", "DD_AGENT_MODE", "false", false}, + {"CLAUDECODE=1", "CLAUDECODE", "1", true}, + {"CURSOR_AGENT=1", "CURSOR_AGENT", "1", true}, + {"AIDER=1", "AIDER", "1", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clearAllAgentEnvVars() + if tt.envVar != "" { + os.Setenv(tt.envVar, tt.envVal) + defer os.Unsetenv(tt.envVar) + } + + got := IsAgentMode() + if got != tt.want { + t.Errorf("IsAgentMode() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDetectAgentInfo(t *testing.T) { + tests := []struct { + name string + envVar string + envVal string + want AgentInfo + }{ + {"no agent", "", "", AgentInfo{}}, + {"claude-code", "CLAUDECODE", "1", AgentInfo{Name: "claude-code", Detected: true}}, + {"cursor", "CURSOR_AGENT", "1", AgentInfo{Name: "cursor", Detected: true}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clearAllAgentEnvVars() + if tt.envVar != "" { + os.Setenv(tt.envVar, tt.envVal) + defer os.Unsetenv(tt.envVar) + } + + got := DetectAgentInfo() + if got != tt.want { + t.Errorf("DetectAgentInfo() = %+v, want %+v", got, tt.want) + } + }) + } +} + func TestGet_Format(t *testing.T) { - // Clear all agent env vars - os.Unsetenv("CLAUDECODE") - os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("CURSOR_AGENT") + clearAllAgentEnvVars() result := Get() - // Verify format: pup/VERSION (go GOVERSION; os OS; arch ARCH) if !strings.HasPrefix(result, "pup/") { t.Errorf("Get() should start with 'pup/', got %q", result) } - if !strings.Contains(result, "(go ") { t.Errorf("Get() should contain '(go ', got %q", result) } - if !strings.Contains(result, "; os ") { t.Errorf("Get() should contain '; os ', got %q", result) } - if !strings.Contains(result, "; arch ") { t.Errorf("Get() should contain '; arch ', got %q", result) } - - // Verify no extra spaces or malformed output if strings.Contains(result, " ") { t.Errorf("Get() should not contain double spaces, got %q", result) } - // Test with agent os.Setenv("CLAUDECODE", "1") defer os.Unsetenv("CLAUDECODE") resultWithAgent := Get() - - // Verify agent is in structured format expectedSuffix := "; ai-agent claude-code)" if !strings.HasSuffix(resultWithAgent, expectedSuffix) { t.Errorf("Get() with agent should end with %q, got %q", expectedSuffix, resultWithAgent) } - // Verify base part is present expectedBase := "pup/" + version.Version + " (go " + runtime.Version() + "; os " + runtime.GOOS + "; arch " + runtime.GOARCH if !strings.HasPrefix(resultWithAgent, expectedBase) { t.Errorf("Get() with agent should start with %q, got %q", expectedBase, resultWithAgent) } } + +func TestIsEnvTruthy(t *testing.T) { + tests := []struct { + val string + want bool + }{ + {"1", true}, + {"true", true}, + {"TRUE", true}, + {"True", true}, + {"0", false}, + {"false", false}, + {"", false}, + {"yes", false}, + } + + for _, tt := range tests { + t.Run(tt.val, func(t *testing.T) { + os.Setenv("TEST_TRUTHY", tt.val) + defer os.Unsetenv("TEST_TRUTHY") + + got := isEnvTruthy("TEST_TRUTHY") + if got != tt.want { + t.Errorf("isEnvTruthy(%q) = %v, want %v", tt.val, got, tt.want) + } + }) + } +} From 6ee54e5b138eeafba260464777e338d7a5bb4748 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sat, 14 Feb 2026 14:26:32 -0600 Subject: [PATCH 2/6] fix(agent): fix formatAPIError for *http.Response and guide section lookup - formatAPIError now handles *http.Response (field-based StatusCode) in addition to the interface-based StatusCode() method. This enables structured agent errors for all API calls including v1 endpoints. - GetGuideSection now tries uppercase, capitalized, and case-insensitive matching for section headings (e.g., "apm" finds "## APM / Traces"). Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/root.go | 19 ++++++++++++---- pkg/agenthelp/guide.go | 44 ++++++++++++++++++++++++++++--------- pkg/agenthelp/guide_test.go | 1 + 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5a213ad4..344d1730 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "net/http" "os" "strings" @@ -432,12 +433,22 @@ func extractAPIErrorBody(err error) string { // and appends contextual guidance based on the HTTP status code. // In agent mode, returns a structured JSON error. func formatAPIError(operation string, err error, response any) error { - type httpResponse interface { - StatusCode() int + // Extract status code from the response. + // The datadog API client returns *http.Response (field) for some APIs + // and types with StatusCode() method for others. + statusCode := 0 + switch r := response.(type) { + case *http.Response: + if r != nil { + statusCode = r.StatusCode + } + case interface{ StatusCode() int }: + if r != nil { + statusCode = r.StatusCode() + } } - if r, ok := response.(httpResponse); ok && r != nil { - statusCode := r.StatusCode() + if statusCode > 0 { apiBody := extractAPIErrorBody(err) // In agent mode, return structured JSON error diff --git a/pkg/agenthelp/guide.go b/pkg/agenthelp/guide.go index 7a478898..72b47235 100644 --- a/pkg/agenthelp/guide.go +++ b/pkg/agenthelp/guide.go @@ -21,20 +21,44 @@ func GetGuide() string { // GetGuideSection returns a specific domain section from the guide. // Returns the full guide if the domain is not found. func GetGuideSection(domain string) string { - // Try capitalized version first (e.g., "## Logs") - capitalized := strings.ToUpper(domain[:1]) + domain[1:] - heading := "## " + capitalized - idx := strings.Index(guideContent, heading) + // Try multiple casing strategies to find the section heading + candidates := []string{ + "## " + strings.ToUpper(domain[:1]) + domain[1:], // "## Logs" + "## " + strings.ToUpper(domain), // "## APM" + "## " + domain, // "## logs" (exact) + } + + var idx int = -1 + var heading string + for _, candidate := range candidates { + idx = strings.Index(guideContent, candidate) + if idx != -1 { + heading = candidate + break + } + } + + // Fall back to case-insensitive line scan if idx == -1 { - // Try exact case - heading = "## " + domain - idx = strings.Index(guideContent, heading) - if idx == -1 { - return guideContent + lowerDomain := strings.ToLower(domain) + for i, line := range strings.Split(guideContent, "\n") { + if strings.HasPrefix(line, "## ") && strings.Contains(strings.ToLower(line), lowerDomain) { + // Reconstruct idx from line number + idx = 0 + for _, l := range strings.Split(guideContent, "\n")[:i] { + idx += len(l) + 1 + } + heading = line + break + } } } - // Find the next ## heading + if idx == -1 { + return guideContent + } + + // Find the next ## heading after this one rest := guideContent[idx+len(heading):] nextSection := strings.Index(rest, "\n## ") if nextSection == -1 { diff --git a/pkg/agenthelp/guide_test.go b/pkg/agenthelp/guide_test.go index 3d6e938c..2f2dde46 100644 --- a/pkg/agenthelp/guide_test.go +++ b/pkg/agenthelp/guide_test.go @@ -37,6 +37,7 @@ func TestGetGuideSection(t *testing.T) { {"logs", "## Logs"}, {"metrics", "## Metrics"}, {"monitors", "## Monitors"}, + {"apm", "## APM"}, {"Logs", "## Logs"}, } From dbba8b518fbbe5544dd0db0412279e11c2a7f6ef Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sat, 14 Feb 2026 18:59:47 -0600 Subject: [PATCH 3/6] feat(agent): intercept --help in agent mode to return JSON schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an AI agent runs `pup --help` or `pup logs --help`, it now gets the structured JSON schema automatically instead of cobra's human-oriented help text. This is detected via agent env vars (CLAUDECODE, CURSOR_AGENT, etc.) before cobra processes the args. - `pup --help` in agent mode → full JSON schema (same as --hlp) - `pup logs --help` in agent mode → logs subtree schema - `pup --help` in human mode → unchanged (normal help text) Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/agent_test.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 27 +++++++++--- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 0099d7d4..dd6b0913 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -272,3 +272,112 @@ func TestAgentModeFlagOverride(t *testing.T) { t.Error("AutoApprove should be true when --agent flag is set") } } + +func TestHelpReturnsSchemaInAgentMode(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + os.Unsetenv("CLAUDECODE") + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + // Set agent env var so --help is intercepted + os.Setenv("CLAUDECODE", "1") + + err := ExecuteWithArgs([]string{"--help"}) + if err != nil { + t.Fatalf("--help in agent mode error: %v", err) + } + + output := buf.String() + + var schema agenthelp.Schema + if err := json.Unmarshal([]byte(output), &schema); err != nil { + t.Fatalf("--help in agent mode should return valid JSON schema, got: %s", output[:200]) + } + + if len(schema.Commands) == 0 { + t.Error("--help schema should have commands") + } +} + +func TestHelpReturnsSchemaSubtreeInAgentMode(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + os.Unsetenv("CLAUDECODE") + }() + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + os.Setenv("CLAUDECODE", "1") + + err := ExecuteWithArgs([]string{"logs", "--help"}) + if err != nil { + t.Fatalf("logs --help in agent mode error: %v", err) + } + + output := buf.String() + + var schema agenthelp.Schema + if err := json.Unmarshal([]byte(output), &schema); err != nil { + t.Fatalf("logs --help in agent mode should return valid JSON schema, got: %s", output[:200]) + } + + if len(schema.Commands) != 1 { + t.Errorf("logs --help should have 1 command, got %d", len(schema.Commands)) + } + if schema.Commands[0].Name != "logs" { + t.Errorf("logs --help command = %q, want 'logs'", schema.Commands[0].Name) + } +} + +func TestHelpUnchangedInHumanMode(t *testing.T) { + origWriter := outputWriter + origCfg := cfg + origClient := ddClient + defer func() { + outputWriter = origWriter + cfg = origCfg + ddClient = origClient + // Clear any agent env vars + os.Unsetenv("CLAUDECODE") + os.Unsetenv("CLAUDE_CODE") + os.Unsetenv("DD_AGENT_MODE") + }() + + // Ensure no agent env vars are set + os.Unsetenv("CLAUDECODE") + os.Unsetenv("CLAUDE_CODE") + os.Unsetenv("DD_AGENT_MODE") + + var buf bytes.Buffer + outputWriter = &buf + cfg = &config.Config{Site: "datadoghq.com"} + ddClient = nil + + // --help in human mode should NOT return JSON + _ = ExecuteWithArgs([]string{"--help"}) + + output := buf.String() + + // Human mode help should contain "Usage:" not JSON + if strings.Contains(output, `"version"`) { + t.Error("--help in human mode should not return JSON schema") + } +} diff --git a/cmd/root.go b/cmd/root.go index 344d1730..9b2fb794 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -102,17 +102,22 @@ func hasFlag(args []string, flag string) bool { // handleHlpArgs processes --hlp by finding the first non-flag arg as subtree name. func handleHlpArgs(args []string) error { - var subtree string + return printHlpSchema(firstNonFlagArg(args)) +} + +// handleHelpAsSchema converts --help/-h to JSON schema output in agent mode. +func handleHelpAsSchema(args []string) error { + return printHlpSchema(firstNonFlagArg(args)) +} + +// firstNonFlagArg returns the first argument that isn't a flag. +func firstNonFlagArg(args []string) string { for _, a := range args { - if a == "--hlp" { - continue - } if !isFlag(a) { - subtree = a - break + return a } } - return printHlpSchema(subtree) + return "" } // printHlpSchema generates and prints the JSON schema for the given subtree. @@ -145,6 +150,14 @@ func ExecuteWithArgs(args []string) error { return handleHlpArgs(args) } + // In agent mode, intercept --help/-h and return structured JSON schema instead + // of cobra's human-oriented help text. This lets agents run the natural + // "pup --help" or "pup logs --help" and get the machine-readable schema + // without needing to know about --hlp. + if (hasFlag(args, "--help") || hasFlag(args, "-h")) && useragent.IsAgentMode() { + return handleHelpAsSchema(args) + } + // IMPORTANT: Aliases are checked LAST to prevent overriding built-in commands. // This ensures that no alias can shadow an existing pup command, even if validation // is bypassed or a new command is added that conflicts with an existing alias. From 779ea9d66306be72138185fcda5a2bc43155a789 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sat, 14 Feb 2026 19:09:43 -0600 Subject: [PATCH 4/6] refactor(cmd): migrate all commands to formatAndPrint Every command that outputs API responses now goes through formatAndPrint instead of calling formatter.FormatOutput directly. This ensures all commands automatically get agent envelope wrapping in agent mode. - 135 formatter.FormatOutput calls replaced with formatAndPrint across 38 command files - Removed unused formatter and fmt imports - Net reduction: -735 lines (868 deleted, 133 added) In agent mode, every command now returns structured output: {"status": "success", "data": ..., "metadata": {...}} Human mode behavior is completely unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/api_keys.go | 22 ++---------- cmd/apm.go | 50 ++++---------------------- cmd/app_keys.go | 22 ++---------- cmd/audit_logs.go | 15 ++------ cmd/auth.go | 9 +---- cmd/cases.go | 78 ++++++---------------------------------- cmd/cicd.go | 29 +++------------ cmd/cloud.go | 22 ++---------- cmd/cost.go | 25 ++----------- cmd/dashboards.go | 25 ++----------- cmd/data_governance.go | 8 +---- cmd/downtime.go | 15 ++------ cmd/error_tracking.go | 17 ++------- cmd/events.go | 22 ++---------- cmd/incidents.go | 25 ++----------- cmd/infrastructure.go | 15 ++------ cmd/integrations.go | 15 ++------ cmd/investigations.go | 22 ++---------- cmd/logs_simple.go | 72 +++++-------------------------------- cmd/metrics.go | 40 +++------------------ cmd/miscellaneous.go | 15 ++------ cmd/monitors.go | 16 ++------- cmd/network.go | 16 ++------- cmd/notebooks.go | 15 ++------ cmd/obs_pipelines.go | 16 ++------- cmd/on_call.go | 50 ++++---------------------- cmd/organizations.go | 15 ++------ cmd/product_analytics.go | 9 +---- cmd/rum.go | 71 ++++++------------------------------ cmd/scorecards.go | 16 ++------- cmd/security.go | 29 +++------------ cmd/service_catalog.go | 15 ++------ cmd/slos.go | 25 ++----------- cmd/synthetics.go | 22 ++---------- cmd/tags.go | 29 +++------------ cmd/usage.go | 15 ++------ cmd/users.go | 22 ++---------- cmd/vulnerabilities.go | 57 +++++------------------------ 38 files changed, 133 insertions(+), 868 deletions(-) diff --git a/cmd/api_keys.go b/cmd/api_keys.go index 36014b72..38cc9c00 100644 --- a/cmd/api_keys.go +++ b/cmd/api_keys.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -112,12 +111,7 @@ func runAPIKeysList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list API keys: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runAPIKeysGet(cmd *cobra.Command, args []string) error { @@ -137,12 +131,7 @@ func runAPIKeysGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get API key: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runAPIKeysCreate(cmd *cobra.Command, args []string) error { @@ -170,12 +159,7 @@ func runAPIKeysCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to create API key: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runAPIKeysDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/apm.go b/cmd/apm.go index 8bd46b6b..55d087a7 100644 --- a/cmd/apm.go +++ b/cmd/apm.go @@ -11,7 +11,6 @@ import ( "strconv" "time" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -580,12 +579,7 @@ func runAPMServicesList(cmd *cobra.Command, args []string) error { err, path, startTime, endTime, envFilter) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runAPMServicesStats(cmd *cobra.Command, args []string) error { @@ -622,12 +616,7 @@ func runAPMServicesStats(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get service stats: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runAPMServicesOperations(cmd *cobra.Command, args []string) error { @@ -670,12 +659,7 @@ func runAPMServicesOperations(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get service operations: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runAPMServicesResources(cmd *cobra.Command, args []string) error { @@ -719,12 +703,7 @@ func runAPMServicesResources(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get service resources: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runAPMEntitiesList(cmd *cobra.Command, args []string) error { @@ -773,12 +752,7 @@ func runAPMEntitiesList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list APM entities: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runAPMDependenciesList(cmd *cobra.Command, args []string) error { @@ -822,12 +796,7 @@ func runAPMDependenciesList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list service dependencies: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runAPMFlowMap(cmd *cobra.Command, args []string) error { @@ -860,10 +829,5 @@ func runAPMFlowMap(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get flow map: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } diff --git a/cmd/app_keys.go b/cmd/app_keys.go index 10baefb0..33a9c9e8 100644 --- a/cmd/app_keys.go +++ b/cmd/app_keys.go @@ -7,7 +7,6 @@ package cmd import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -132,12 +131,7 @@ func runAppKeysList(cmd *cobra.Command, args []string) error { return formatAPIError("list app key registrations", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runAppKeysGet(cmd *cobra.Command, args []string) error { @@ -154,12 +148,7 @@ func runAppKeysGet(cmd *cobra.Command, args []string) error { return formatAPIError("get app key registration", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runAppKeysRegister(cmd *cobra.Command, args []string) error { @@ -176,12 +165,7 @@ func runAppKeysRegister(cmd *cobra.Command, args []string) error { return formatAPIError("register app key", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runAppKeysUnregister(cmd *cobra.Command, args []string) error { diff --git a/cmd/audit_logs.go b/cmd/audit_logs.go index 976126a1..121b7553 100644 --- a/cmd/audit_logs.go +++ b/cmd/audit_logs.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -102,12 +101,7 @@ func runAuditLogsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list audit logs: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runAuditLogsSearch(cmd *cobra.Command, args []string) error { @@ -140,10 +134,5 @@ func runAuditLogsSearch(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to search audit logs: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/auth.go b/cmd/auth.go index 400a7c05..80357294 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -17,7 +17,6 @@ import ( "github.com/DataDog/pup/pkg/auth/oauth" "github.com/DataDog/pup/pkg/auth/storage" "github.com/DataDog/pup/pkg/auth/types" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -455,13 +454,7 @@ func runAuthStatus(cmd *cobra.Command, args []string) error { fmt.Printf(" Token expires in: %s\n", timeLeft.Round(time.Second)) } - output, err := formatter.FormatOutput(status, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - fmt.Printf("\n%s\n", output) - return nil + return formatAndPrint(status, nil) } func runAuthLogout(cmd *cobra.Command, args []string) error { diff --git a/cmd/cases.go b/cmd/cases.go index b6153f0d..cb30b463 100644 --- a/cmd/cases.go +++ b/cmd/cases.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -362,12 +361,7 @@ func runCasesSearch(cmd *cobra.Command, args []string) error { return formatAPIError("search cases", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesGet(cmd *cobra.Command, args []string) error { @@ -384,12 +378,7 @@ func runCasesGet(cmd *cobra.Command, args []string) error { return formatAPIError("get case", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesCreate(cmd *cobra.Command, args []string) error { @@ -422,12 +411,7 @@ func runCasesCreate(cmd *cobra.Command, args []string) error { return formatAPIError("create case", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesArchive(cmd *cobra.Command, args []string) error { @@ -446,12 +430,7 @@ func runCasesArchive(cmd *cobra.Command, args []string) error { return formatAPIError("archive case", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesUnarchive(cmd *cobra.Command, args []string) error { @@ -470,12 +449,7 @@ func runCasesUnarchive(cmd *cobra.Command, args []string) error { return formatAPIError("unarchive case", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesAssign(cmd *cobra.Command, args []string) error { @@ -497,12 +471,7 @@ func runCasesAssign(cmd *cobra.Command, args []string) error { return formatAPIError("assign case", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesUpdateTitle(cmd *cobra.Command, args []string) error { @@ -524,12 +493,7 @@ func runCasesUpdateTitle(cmd *cobra.Command, args []string) error { return formatAPIError("update case title", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesUpdatePriority(cmd *cobra.Command, args []string) error { @@ -556,12 +520,7 @@ func runCasesUpdatePriority(cmd *cobra.Command, args []string) error { return formatAPIError("update case priority", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } // Project implementations @@ -577,12 +536,7 @@ func runCasesProjectsList(cmd *cobra.Command, args []string) error { return formatAPIError("list projects", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesProjectsGet(cmd *cobra.Command, args []string) error { @@ -599,12 +553,7 @@ func runCasesProjectsGet(cmd *cobra.Command, args []string) error { return formatAPIError("get project", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesProjectsCreate(cmd *cobra.Command, args []string) error { @@ -624,12 +573,7 @@ func runCasesProjectsCreate(cmd *cobra.Command, args []string) error { return formatAPIError("create project", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCasesProjectsDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/cicd.go b/cmd/cicd.go index 9b816343..75a0976b 100644 --- a/cmd/cicd.go +++ b/cmd/cicd.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -166,12 +165,7 @@ func runCICDPipelinesList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list pipelines: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runCICDPipelinesGet(cmd *cobra.Command, args []string) error { @@ -200,12 +194,7 @@ func runCICDPipelinesGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get pipeline: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runCICDEventsSearch(cmd *cobra.Command, args []string) error { @@ -248,12 +237,7 @@ func runCICDEventsSearch(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to search events: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runCICDEventsAggregate(cmd *cobra.Command, args []string) error { @@ -302,12 +286,7 @@ func runCICDEventsAggregate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to aggregate events: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func buildComputeAggregation(compute string) (*datadogV2.CIAppCompute, error) { diff --git a/cmd/cloud.go b/cmd/cloud.go index 0f3d4af5..433667ba 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -96,12 +95,7 @@ func runCloudAWSList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list AWS integrations: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runCloudGCPList(cmd *cobra.Command, args []string) error { @@ -119,12 +113,7 @@ func runCloudGCPList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list GCP integrations: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runCloudAzureList(cmd *cobra.Command, args []string) error { @@ -142,10 +131,5 @@ func runCloudAzureList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list Azure integrations: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/cost.go b/cmd/cost.go index 13290a5e..5b928b6c 100644 --- a/cmd/cost.go +++ b/cmd/cost.go @@ -10,7 +10,6 @@ import ( "time" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -142,13 +141,7 @@ func runCostProjected(cmd *cobra.Command, args []string) error { return formatAPIError("get projected cost", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCostAttribution(cmd *cobra.Command, args []string) error { @@ -183,13 +176,7 @@ func runCostAttribution(cmd *cobra.Command, args []string) error { return formatAPIError("get cost attribution", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runCostByOrg(cmd *cobra.Command, args []string) error { @@ -253,11 +240,5 @@ func runCostByOrg(cmd *cobra.Command, args []string) error { return formatAPIError(fmt.Sprintf("get %s cost by org", costView), err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/dashboards.go b/cmd/dashboards.go index cbb8bb48..2c8fab0c 100644 --- a/cmd/dashboards.go +++ b/cmd/dashboards.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -212,13 +211,7 @@ func runDashboardsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list dashboards: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runDashboardsGet(cmd *cobra.Command, args []string) error { @@ -238,13 +231,7 @@ func runDashboardsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get dashboard: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runDashboardsDelete(cmd *cobra.Command, args []string) error { @@ -282,11 +269,5 @@ func runDashboardsDelete(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to delete dashboard: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/data_governance.go b/cmd/data_governance.go index 8d089662..845c4896 100644 --- a/cmd/data_governance.go +++ b/cmd/data_governance.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -72,10 +71,5 @@ func runDataGovernanceScannerRulesList(cmd *cobra.Command, args []string) error return fmt.Errorf("failed to list scanning rules: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/downtime.go b/cmd/downtime.go index 9145070e..9e311606 100644 --- a/cmd/downtime.go +++ b/cmd/downtime.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -81,12 +80,7 @@ func runDowntimeList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list downtimes: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runDowntimeGet(cmd *cobra.Command, args []string) error { @@ -105,12 +99,7 @@ func runDowntimeGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get downtime: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runDowntimeCancel(cmd *cobra.Command, args []string) error { diff --git a/cmd/error_tracking.go b/cmd/error_tracking.go index 5cdc66b9..12db778f 100644 --- a/cmd/error_tracking.go +++ b/cmd/error_tracking.go @@ -7,7 +7,6 @@ package cmd import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/DataDog/pup/pkg/util" "github.com/spf13/cobra" ) @@ -167,13 +166,7 @@ func runErrorTrackingIssuesSearch(cmd *cobra.Command, args []string) error { resp.Data = resp.Data[:etLimit] } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runErrorTrackingIssuesGet(cmd *cobra.Command, args []string) error { @@ -199,11 +192,5 @@ func runErrorTrackingIssuesGet(cmd *cobra.Command, args []string) error { return formatAPIError("get error tracking issue", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/events.go b/cmd/events.go index bd86ff8d..77b7122a 100644 --- a/cmd/events.go +++ b/cmd/events.go @@ -11,7 +11,6 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -117,12 +116,7 @@ func runEventsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list events: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runEventsSearch(cmd *cobra.Command, args []string) error { @@ -170,12 +164,7 @@ func runEventsSearch(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to search events: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runEventsGet(cmd *cobra.Command, args []string) error { @@ -194,10 +183,5 @@ func runEventsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get event: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/incidents.go b/cmd/incidents.go index 845b2769..2a80eb1b 100644 --- a/cmd/incidents.go +++ b/cmd/incidents.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -295,13 +294,7 @@ func runIncidentsList(cmd *cobra.Command, args []string) error { return formatAPIError("list incidents", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runIncidentsGet(cmd *cobra.Command, args []string) error { @@ -318,13 +311,7 @@ func runIncidentsGet(cmd *cobra.Command, args []string) error { return formatAPIError("get incident", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } // Attachment implementations @@ -342,13 +329,7 @@ func runIncidentsAttachmentsList(cmd *cobra.Command, args []string) error { return formatAPIError("list incident attachments", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runIncidentsAttachmentsDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/infrastructure.go b/cmd/infrastructure.go index 42cb04c9..e3f6e08e 100644 --- a/cmd/infrastructure.go +++ b/cmd/infrastructure.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -97,12 +96,7 @@ func runInfrastructureHostsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list hosts: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runInfrastructureHostsGet(cmd *cobra.Command, args []string) error { @@ -122,10 +116,5 @@ func runInfrastructureHostsGet(cmd *cobra.Command, args []string) error { } _ = hostname // Use hostname for filtering in actual implementation - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/integrations.go b/cmd/integrations.go index 8deb1923..720f93d5 100644 --- a/cmd/integrations.go +++ b/cmd/integrations.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -96,12 +95,7 @@ func runIntegrationsSlackList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list Slack channels: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runIntegrationsPagerDutyList(cmd *cobra.Command, args []string) error { @@ -125,10 +119,5 @@ func runIntegrationsWebhooksList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list webhooks: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/investigations.go b/cmd/investigations.go index 96875d97..06ae6f20 100644 --- a/cmd/investigations.go +++ b/cmd/investigations.go @@ -12,7 +12,6 @@ import ( "io" "net/http" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -117,12 +116,7 @@ func runInvestigationsTrigger(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to trigger investigation: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runInvestigationsGet(cmd *cobra.Command, args []string) error { @@ -143,12 +137,7 @@ func runInvestigationsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get investigation: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func runInvestigationsList(cmd *cobra.Command, args []string) error { @@ -173,12 +162,7 @@ func runInvestigationsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list investigations: %w", err) } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } func buildTriggerRequestBody() (map[string]any, error) { diff --git a/cmd/logs_simple.go b/cmd/logs_simple.go index 47ee0450..a5161351 100644 --- a/cmd/logs_simple.go +++ b/cmd/logs_simple.go @@ -954,13 +954,7 @@ func runLogsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list logs: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsQuery(cmd *cobra.Command, args []string) error { @@ -1027,13 +1021,7 @@ func runLogsQuery(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to query logs: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsAggregate(cmd *cobra.Command, args []string) error { @@ -1126,13 +1114,7 @@ func runLogsAggregate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to aggregate logs: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsArchivesList(cmd *cobra.Command, args []string) error { @@ -1151,13 +1133,7 @@ func runLogsArchivesList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list log archives: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsArchivesGet(cmd *cobra.Command, args []string) error { @@ -1177,13 +1153,7 @@ func runLogsArchivesGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get log archive: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsArchivesDelete(cmd *cobra.Command, args []string) error { @@ -1241,13 +1211,7 @@ func runLogsCustomDestinationsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list custom destinations: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsCustomDestinationsGet(cmd *cobra.Command, args []string) error { @@ -1267,13 +1231,7 @@ func runLogsCustomDestinationsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get custom destination: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsMetricsList(cmd *cobra.Command, args []string) error { @@ -1292,13 +1250,7 @@ func runLogsMetricsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list log-based metrics: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsMetricsGet(cmd *cobra.Command, args []string) error { @@ -1318,13 +1270,7 @@ func runLogsMetricsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get log-based metric: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runLogsMetricsDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/metrics.go b/cmd/metrics.go index ce7a48cb..f50e3b3e 100644 --- a/cmd/metrics.go +++ b/cmd/metrics.go @@ -623,13 +623,7 @@ func runMetricsSearch(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to search metrics: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } // matchMetricName checks if a metric name matches a wildcard pattern. @@ -754,13 +748,7 @@ func runMetricsList(cmd *cobra.Command, args []string) error { } } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } // runMetricsMetadataGet executes the metadata get command @@ -786,13 +774,7 @@ func runMetricsMetadataGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get metric metadata: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } // runMetricsMetadataUpdate executes the metadata update command @@ -842,13 +824,7 @@ func runMetricsMetadataUpdate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to update metric metadata: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } // runMetricsSubmit executes the metrics submit command @@ -943,13 +919,7 @@ func runMetricsSubmit(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to submit metrics: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } // runMetricsTagsList executes the tags list command diff --git a/cmd/miscellaneous.go b/cmd/miscellaneous.go index 22b93091..b2fce0a2 100644 --- a/cmd/miscellaneous.go +++ b/cmd/miscellaneous.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -66,12 +65,7 @@ func runMiscIPRanges(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get IP ranges: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runMiscStatus(cmd *cobra.Command, args []string) error { @@ -80,10 +74,5 @@ func runMiscStatus(cmd *cobra.Command, args []string) error { "message": "API is operational", } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(result, nil) } diff --git a/cmd/monitors.go b/cmd/monitors.go index e90c3f95..084b54dc 100644 --- a/cmd/monitors.go +++ b/cmd/monitors.go @@ -400,13 +400,7 @@ func runMonitorsDelete(cmd *cobra.Command, args []string) error { return formatAPIError("delete monitor", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runMonitorsSearch(cmd *cobra.Command, args []string) error { @@ -436,11 +430,5 @@ func runMonitorsSearch(cmd *cobra.Command, args []string) error { return formatAPIError("search monitors", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/network.go b/cmd/network.go index 32b87833..b2e90d19 100644 --- a/cmd/network.go +++ b/cmd/network.go @@ -6,9 +6,7 @@ package cmd import ( - "fmt" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -73,12 +71,7 @@ func runNetworkFlowsList(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runNetworkDevicesList(cmd *cobra.Command, args []string) error { @@ -89,10 +82,5 @@ func runNetworkDevicesList(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } diff --git a/cmd/notebooks.go b/cmd/notebooks.go index 8fa7529b..c0a4a0ed 100644 --- a/cmd/notebooks.go +++ b/cmd/notebooks.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -78,12 +77,7 @@ func runNotebooksList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list notebooks: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runNotebooksGet(cmd *cobra.Command, args []string) error { @@ -102,12 +96,7 @@ func runNotebooksGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get notebook: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runNotebooksDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/obs_pipelines.go b/cmd/obs_pipelines.go index 66279a2f..cd38e3a9 100644 --- a/cmd/obs_pipelines.go +++ b/cmd/obs_pipelines.go @@ -6,9 +6,7 @@ package cmd import ( - "fmt" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -62,12 +60,7 @@ func runObsPipelinesList(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runObsPipelinesGet(cmd *cobra.Command, args []string) error { @@ -82,10 +75,5 @@ func runObsPipelinesGet(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } diff --git a/cmd/on_call.go b/cmd/on_call.go index f5f77a80..6350e642 100644 --- a/cmd/on_call.go +++ b/cmd/on_call.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -312,12 +311,7 @@ func runOnCallTeamsList(cmd *cobra.Command, args []string) error { return formatAPIError("list teams", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runOnCallTeamsGet(cmd *cobra.Command, args []string) error { @@ -333,12 +327,7 @@ func runOnCallTeamsGet(cmd *cobra.Command, args []string) error { return formatAPIError("get team", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runOnCallTeamsCreate(cmd *cobra.Command, args []string) error { @@ -369,12 +358,7 @@ func runOnCallTeamsCreate(cmd *cobra.Command, args []string) error { return formatAPIError("create team", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runOnCallTeamsUpdate(cmd *cobra.Command, args []string) error { @@ -407,12 +391,7 @@ func runOnCallTeamsUpdate(cmd *cobra.Command, args []string) error { return formatAPIError("update team", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runOnCallTeamsDelete(cmd *cobra.Command, args []string) error { @@ -479,12 +458,7 @@ func runOnCallTeamsMembershipsList(cmd *cobra.Command, args []string) error { return formatAPIError("list team memberships", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runOnCallTeamsMembershipsAdd(cmd *cobra.Command, args []string) error { @@ -524,12 +498,7 @@ func runOnCallTeamsMembershipsAdd(cmd *cobra.Command, args []string) error { return formatAPIError("add team member", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runOnCallTeamsMembershipsUpdate(cmd *cobra.Command, args []string) error { @@ -558,12 +527,7 @@ func runOnCallTeamsMembershipsUpdate(cmd *cobra.Command, args []string) error { return formatAPIError("update team membership", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runOnCallTeamsMembershipsRemove(cmd *cobra.Command, args []string) error { diff --git a/cmd/organizations.go b/cmd/organizations.go index ab13638b..70ec2efa 100644 --- a/cmd/organizations.go +++ b/cmd/organizations.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -66,12 +65,7 @@ func runOrganizationsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get organization: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runOrganizationsList(cmd *cobra.Command, args []string) error { @@ -89,10 +83,5 @@ func runOrganizationsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list organizations: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/product_analytics.go b/cmd/product_analytics.go index c01c7b88..ce0c84ae 100644 --- a/cmd/product_analytics.go +++ b/cmd/product_analytics.go @@ -10,7 +10,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -146,11 +145,5 @@ func runProductAnalyticsEventsSend(cmd *cobra.Command, args []string) error { return formatAPIError("submit product analytics event", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/rum.go b/cmd/rum.go index 25265575..38c3d38f 100644 --- a/cmd/rum.go +++ b/cmd/rum.go @@ -11,7 +11,6 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadog" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/DataDog/pup/pkg/util" "github.com/spf13/cobra" ) @@ -402,12 +401,7 @@ func runRumAppsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list RUM applications: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runRumAppsGet(cmd *cobra.Command, args []string) error { @@ -426,12 +420,7 @@ func runRumAppsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get RUM application: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runRumAppsCreate(cmd *cobra.Command, args []string) error { @@ -465,12 +454,7 @@ func runRumAppsCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to create RUM application: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runRumAppsUpdate(cmd *cobra.Command, args []string) error { @@ -508,12 +492,7 @@ func runRumAppsUpdate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to update RUM application: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runRumAppsDelete(cmd *cobra.Command, args []string) error { @@ -563,12 +542,7 @@ func runRumMetricsList(cmd *cobra.Command, args []string) error { return formatAPIError("list RUM metrics", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runRumMetricsGet(cmd *cobra.Command, args []string) error { @@ -584,12 +558,7 @@ func runRumMetricsGet(cmd *cobra.Command, args []string) error { return formatAPIError("get RUM metric", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runRumMetricsCreate(cmd *cobra.Command, args []string) error { @@ -617,12 +586,7 @@ func runRumRetentionFiltersList(cmd *cobra.Command, args []string) error { return formatAPIError("list RUM retention filters", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runRumRetentionFiltersGet(cmd *cobra.Command, args []string) error { @@ -638,12 +602,7 @@ func runRumRetentionFiltersGet(cmd *cobra.Command, args []string) error { return formatAPIError("get RUM retention filter", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runRumRetentionFiltersCreate(cmd *cobra.Command, args []string) error { @@ -700,12 +659,7 @@ func runRumSessionsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list RUM sessions: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runRumSessionsSearch(cmd *cobra.Command, args []string) error { @@ -749,12 +703,7 @@ func runRumSessionsSearch(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to search RUM sessions: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } // RUM Playlists (Placeholder) diff --git a/cmd/scorecards.go b/cmd/scorecards.go index dc11f729..276d7c32 100644 --- a/cmd/scorecards.go +++ b/cmd/scorecards.go @@ -6,9 +6,7 @@ package cmd import ( - "fmt" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -62,12 +60,7 @@ func runScorecardsList(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runScorecardsGet(cmd *cobra.Command, args []string) error { @@ -82,10 +75,5 @@ func runScorecardsGet(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } diff --git a/cmd/security.go b/cmd/security.go index e00a832a..e94904ec 100644 --- a/cmd/security.go +++ b/cmd/security.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -136,12 +135,7 @@ func runSecurityRulesList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list security rules: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runSecurityRulesGet(cmd *cobra.Command, args []string) error { @@ -160,12 +154,7 @@ func runSecurityRulesGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get security rule: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runSecuritySignalsList(cmd *cobra.Command, args []string) error { @@ -183,12 +172,7 @@ func runSecuritySignalsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list security signals: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runSecurityFindingsSearch(cmd *cobra.Command, args []string) error { @@ -231,10 +215,5 @@ func runSecurityFindingsSearch(cmd *cobra.Command, args []string) error { return formatAPIError("search security findings", err, r) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/service_catalog.go b/cmd/service_catalog.go index 1d8c7488..16e9cc76 100644 --- a/cmd/service_catalog.go +++ b/cmd/service_catalog.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -70,12 +69,7 @@ func runServiceCatalogList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list services: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runServiceCatalogGet(cmd *cobra.Command, args []string) error { @@ -94,10 +88,5 @@ func runServiceCatalogGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get service: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/slos.go b/cmd/slos.go index 52982b28..b90d7157 100644 --- a/cmd/slos.go +++ b/cmd/slos.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -230,13 +229,7 @@ func runSlosList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list SLOs: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runSlosGet(cmd *cobra.Command, args []string) error { @@ -256,13 +249,7 @@ func runSlosGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get SLO: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runSlosDelete(cmd *cobra.Command, args []string) error { @@ -300,11 +287,5 @@ func runSlosDelete(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to delete SLO: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/synthetics.go b/cmd/synthetics.go index 2a7f1e40..fee3e2d9 100644 --- a/cmd/synthetics.go +++ b/cmd/synthetics.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -92,12 +91,7 @@ func runSyntheticsTestsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list synthetic tests: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runSyntheticsTestsGet(cmd *cobra.Command, args []string) error { @@ -116,12 +110,7 @@ func runSyntheticsTestsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get synthetic test: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runSyntheticsLocationsList(cmd *cobra.Command, args []string) error { @@ -139,10 +128,5 @@ func runSyntheticsLocationsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list locations: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/tags.go b/cmd/tags.go index cacaba7f..ae653d25 100644 --- a/cmd/tags.go +++ b/cmd/tags.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -95,12 +94,7 @@ func runTagsList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list host tags: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runTagsGet(cmd *cobra.Command, args []string) error { @@ -119,12 +113,7 @@ func runTagsGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get host tags: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runTagsAdd(cmd *cobra.Command, args []string) error { @@ -149,12 +138,7 @@ func runTagsAdd(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to add host tags: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runTagsUpdate(cmd *cobra.Command, args []string) error { @@ -179,12 +163,7 @@ func runTagsUpdate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to update host tags: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runTagsDelete(cmd *cobra.Command, args []string) error { diff --git a/cmd/usage.go b/cmd/usage.go index cc5dfd4f..0e66647c 100644 --- a/cmd/usage.go +++ b/cmd/usage.go @@ -10,7 +10,6 @@ import ( "time" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -90,12 +89,7 @@ func runUsageSummary(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get usage summary: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runUsageHourly(cmd *cobra.Command, args []string) error { @@ -125,10 +119,5 @@ func runUsageHourly(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get hourly usage: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/users.go b/cmd/users.go index 5b9312ae..99d595f7 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -83,12 +82,7 @@ func runUsersList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list users: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runUsersGet(cmd *cobra.Command, args []string) error { @@ -107,12 +101,7 @@ func runUsersGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to get user: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } func runUsersRolesList(cmd *cobra.Command, args []string) error { @@ -130,10 +119,5 @@ func runUsersRolesList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list roles: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - printOutput("%s\n", output) - return nil + return formatAndPrint(resp, nil) } diff --git a/cmd/vulnerabilities.go b/cmd/vulnerabilities.go index f5ae694b..1b231f5c 100644 --- a/cmd/vulnerabilities.go +++ b/cmd/vulnerabilities.go @@ -9,7 +9,6 @@ import ( "fmt" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" - "github.com/DataDog/pup/pkg/formatter" "github.com/spf13/cobra" ) @@ -152,12 +151,7 @@ func runStaticAnalysisASTList(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runStaticAnalysisASTGet(cmd *cobra.Command, args []string) error { @@ -172,12 +166,7 @@ func runStaticAnalysisASTGet(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runStaticAnalysisCustomRulesetsList(cmd *cobra.Command, args []string) error { @@ -195,12 +184,7 @@ func runStaticAnalysisCustomRulesetsList(cmd *cobra.Command, args []string) erro return fmt.Errorf("failed to list custom rulesets: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runStaticAnalysisCustomRulesetsGet(cmd *cobra.Command, args []string) error { @@ -219,12 +203,7 @@ func runStaticAnalysisCustomRulesetsGet(cmd *cobra.Command, args []string) error return fmt.Errorf("failed to get custom ruleset: %w", err) } - output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(resp, nil) } func runStaticAnalysisSCAList(cmd *cobra.Command, args []string) error { @@ -240,12 +219,7 @@ func runStaticAnalysisSCAList(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runStaticAnalysisSCAGet(cmd *cobra.Command, args []string) error { @@ -260,12 +234,7 @@ func runStaticAnalysisSCAGet(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runStaticAnalysisCoverageList(cmd *cobra.Command, args []string) error { @@ -282,12 +251,7 @@ func runStaticAnalysisCoverageList(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } func runStaticAnalysisCoverageGet(cmd *cobra.Command, args []string) error { @@ -302,10 +266,5 @@ func runStaticAnalysisCoverageGet(cmd *cobra.Command, args []string) error { }, } - output, err := formatter.FormatOutput(result, formatter.OutputFormat(outputFormat)) - if err != nil { - return err - } - fmt.Println(output) - return nil + return formatAndPrint(result, nil) } From 755fa38899bbacd2e5c2224398051f347695cf00 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Mon, 16 Feb 2026 18:36:39 -0600 Subject: [PATCH 5/6] refactor(agent): remove --hlp flag, use --help auto-detection instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In agent mode (auto-detected or FORCE_AGENT_MODE=1), --help now returns structured JSON schema. This eliminates the need for a separate --hlp flag — agents can use the standard --help and get machine-readable output automatically. - Remove --hlp flag, HandleHlpFlag, errHlpHandled sentinel - Rename DD_AGENT_MODE env var to FORCE_AGENT_MODE for testing - Update tests to use FORCE_AGENT_MODE and --help instead of --hlp - Update guide.md and docstrings Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/agent.go | 41 ++++------------------ cmd/agent_test.go | 32 +++++++++++------- cmd/root.go | 60 ++++----------------------------- pkg/agenthelp/agenthelp.go | 2 +- pkg/agenthelp/guide.md | 8 ++--- pkg/useragent/useragent.go | 4 +-- pkg/useragent/useragent_test.go | 10 +++--- 7 files changed, 45 insertions(+), 112 deletions(-) diff --git a/cmd/agent.go b/cmd/agent.go index 56b1d1d6..3f4be82f 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -20,8 +20,11 @@ var agentCmd = &cobra.Command{ Short: "Agent tooling: schema, guide, and diagnostics for AI coding assistants", Long: `Commands for AI coding assistants to interact with pup efficiently. +In agent mode (auto-detected or via --agent / FORCE_AGENT_MODE=1), +--help returns structured JSON schema instead of human-readable text. + COMMANDS: - schema Output the complete command schema as JSON (same as --hlp) + schema Output the complete command schema as JSON guide Output the comprehensive steering guide EXAMPLES: @@ -43,8 +46,9 @@ var agentSchemaCmd = &cobra.Command{ Short: "Output command schema as JSON", Long: `Output the complete pup command schema as structured JSON. -This is the same output as 'pup --hlp' and includes all commands, flags, -query syntax, time formats, workflows, best practices, and anti-patterns. +Includes all commands, flags, query syntax, time formats, workflows, +best practices, and anti-patterns. This is the same output returned +by --help when running in agent mode. FLAGS: --compact Output minimal schema (command names and flags only) @@ -108,34 +112,3 @@ func runAgentGuide(cmd *cobra.Command, args []string) error { return nil } -// HandleHlpFlag processes the --hlp flag on any command. -// It generates the schema for the full tree or a subtree and exits. -// Returns true if --hlp was handled (caller should return). -func HandleHlpFlag(cmd *cobra.Command) (bool, error) { - if !hlpFlag { - return false, nil - } - - root := cmd.Root() - - var data interface{} - // If --hlp is on a subcommand, generate subtree schema - if cmd != root && cmd.Parent() == root { - schema := agenthelp.GenerateSubtreeSchema(root, cmd.Name()) - if schema != nil { - data = schema - } else { - data = agenthelp.GenerateSchema(root) - } - } else { - data = agenthelp.GenerateSchema(root) - } - - out, err := json.MarshalIndent(data, "", " ") - if err != nil { - return true, fmt.Errorf("failed to marshal schema: %w", err) - } - - printOutput("%s\n", string(out)) - return true, nil -} diff --git a/cmd/agent_test.go b/cmd/agent_test.go index dd6b0913..20daa5fb 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -146,7 +146,7 @@ func TestAgentGuideDomain(t *testing.T) { } } -func TestHlpFlag(t *testing.T) { +func TestForceAgentModeHelp(t *testing.T) { origWriter := outputWriter origCfg := cfg origClient := ddClient @@ -154,6 +154,7 @@ func TestHlpFlag(t *testing.T) { outputWriter = origWriter cfg = origCfg ddClient = origClient + os.Unsetenv("FORCE_AGENT_MODE") }() var buf bytes.Buffer @@ -161,24 +162,26 @@ func TestHlpFlag(t *testing.T) { cfg = &config.Config{Site: "datadoghq.com"} ddClient = nil - err := ExecuteWithArgs([]string{"--hlp"}) + os.Setenv("FORCE_AGENT_MODE", "1") + + err := ExecuteWithArgs([]string{"--help"}) if err != nil { - t.Fatalf("--hlp error: %v", err) + t.Fatalf("--help with FORCE_AGENT_MODE error: %v", err) } output := buf.String() var schema agenthelp.Schema if err := json.Unmarshal([]byte(output), &schema); err != nil { - t.Fatalf("--hlp output is not valid JSON: %v", err) + t.Fatalf("--help with FORCE_AGENT_MODE should return JSON schema: %v", err) } if len(schema.Commands) == 0 { - t.Error("--hlp schema commands should not be empty") + t.Error("schema commands should not be empty") } } -func TestHlpFlagSubtree(t *testing.T) { +func TestForceAgentModeHelpSubtree(t *testing.T) { origWriter := outputWriter origCfg := cfg origClient := ddClient @@ -186,6 +189,7 @@ func TestHlpFlagSubtree(t *testing.T) { outputWriter = origWriter cfg = origCfg ddClient = origClient + os.Unsetenv("FORCE_AGENT_MODE") }() var buf bytes.Buffer @@ -193,23 +197,25 @@ func TestHlpFlagSubtree(t *testing.T) { cfg = &config.Config{Site: "datadoghq.com"} ddClient = nil - err := ExecuteWithArgs([]string{"monitors", "--hlp"}) + os.Setenv("FORCE_AGENT_MODE", "1") + + err := ExecuteWithArgs([]string{"monitors", "--help"}) if err != nil { - t.Fatalf("monitors --hlp error: %v", err) + t.Fatalf("monitors --help with FORCE_AGENT_MODE error: %v", err) } output := buf.String() var schema agenthelp.Schema if err := json.Unmarshal([]byte(output), &schema); err != nil { - t.Fatalf("monitors --hlp output is not valid JSON: %v", err) + t.Fatalf("monitors --help with FORCE_AGENT_MODE should return JSON: %v", err) } if len(schema.Commands) != 1 { - t.Errorf("monitors --hlp should have 1 command, got %d", len(schema.Commands)) + t.Errorf("monitors --help should have 1 command, got %d", len(schema.Commands)) } if schema.Commands[0].Name != "monitors" { - t.Errorf("monitors --hlp command name = %q, want 'monitors'", schema.Commands[0].Name) + t.Errorf("monitors --help command name = %q, want 'monitors'", schema.Commands[0].Name) } } @@ -358,13 +364,13 @@ func TestHelpUnchangedInHumanMode(t *testing.T) { // Clear any agent env vars os.Unsetenv("CLAUDECODE") os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("DD_AGENT_MODE") + os.Unsetenv("FORCE_AGENT_MODE") }() // Ensure no agent env vars are set os.Unsetenv("CLAUDECODE") os.Unsetenv("CLAUDE_CODE") - os.Unsetenv("DD_AGENT_MODE") + os.Unsetenv("FORCE_AGENT_MODE") var buf bytes.Buffer outputWriter = &buf diff --git a/cmd/root.go b/cmd/root.go index 9b2fb794..6f7fe197 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,7 +39,6 @@ var ( outputFormat string autoApprove bool agentFlag bool - hlpFlag bool // Dependency injection points for testing clientFactory = defaultClientFactory @@ -55,39 +54,11 @@ var rootCmd = &cobra.Command{ with Datadog APIs. It supports both API key and OAuth2 authentication.`, Version: version.Version, SilenceUsage: true, // Don't show usage on errors, only on --help or invalid args - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - handled, err := HandleHlpFlag(cmd) - if err != nil { - return err - } - if handled { - cmd.SilenceErrors = true - return errHlpHandled - } - return nil - }, - // RunE is needed so that 'pup --hlp' (no subcommand) invokes the PersistentPreRunE. - // Without RunE, cobra shows help text instead. - RunE: func(cmd *cobra.Command, args []string) error { - return cmd.Help() - }, } -// errHlpHandled is a sentinel error returned after --hlp output to stop execution. -// Execute/ExecuteWithArgs checks for this and treats it as a success exit. -var errHlpHandled = errors.New("hlp handled") - // Execute adds all child commands to the root command and sets flags appropriately. func Execute() error { - return suppressHlpError(ExecuteWithArgs(os.Args[1:])) -} - -// suppressHlpError converts the --hlp sentinel error to nil so callers see success. -func suppressHlpError(err error) error { - if errors.Is(err, errHlpHandled) { - return nil - } - return err + return ExecuteWithArgs(os.Args[1:]) } // hasFlag checks if a flag is present in the args. @@ -100,16 +71,6 @@ func hasFlag(args []string, flag string) bool { return false } -// handleHlpArgs processes --hlp by finding the first non-flag arg as subtree name. -func handleHlpArgs(args []string) error { - return printHlpSchema(firstNonFlagArg(args)) -} - -// handleHelpAsSchema converts --help/-h to JSON schema output in agent mode. -func handleHelpAsSchema(args []string) error { - return printHlpSchema(firstNonFlagArg(args)) -} - // firstNonFlagArg returns the first argument that isn't a flag. func firstNonFlagArg(args []string) string { for _, a := range args { @@ -120,8 +81,8 @@ func firstNonFlagArg(args []string) string { return "" } -// printHlpSchema generates and prints the JSON schema for the given subtree. -func printHlpSchema(subtree string) error { +// printAgentSchema generates and prints the JSON schema for the given subtree. +func printAgentSchema(subtree string) error { var data interface{} if subtree != "" { schema := agenthelp.GenerateSubtreeSchema(rootCmd, subtree) @@ -144,18 +105,12 @@ func printHlpSchema(subtree string) error { // ExecuteWithArgs executes the root command with the given arguments func ExecuteWithArgs(args []string) error { - // Handle --hlp before cobra processes args, because group commands (e.g. "logs") - // have no RunE and cobra would show help text instead of invoking PersistentPreRunE. - if hasFlag(args, "--hlp") { - return handleHlpArgs(args) - } - // In agent mode, intercept --help/-h and return structured JSON schema instead // of cobra's human-oriented help text. This lets agents run the natural // "pup --help" or "pup logs --help" and get the machine-readable schema - // without needing to know about --hlp. + // without needing to know about a special flag. if (hasFlag(args, "--help") || hasFlag(args, "-h")) && useragent.IsAgentMode() { - return handleHelpAsSchema(args) + return printAgentSchema(firstNonFlagArg(args)) } // IMPORTANT: Aliases are checked LAST to prevent overriding built-in commands. @@ -173,13 +128,13 @@ func ExecuteWithArgs(args []string) error { // Expand the alias by replacing args[0] with the alias command expandedArgs := expandAlias(aliasCommand, args[1:]) rootCmd.SetArgs(expandedArgs) - return suppressHlpError(rootCmd.Execute()) + return rootCmd.Execute() } } // Not an alias or is a built-in command, execute normally rootCmd.SetArgs(args) - return suppressHlpError(rootCmd.Execute()) + return rootCmd.Execute() } // expandAlias expands an alias command and appends additional arguments @@ -270,7 +225,6 @@ func init() { rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "json", "Output format (json, table, yaml)") rootCmd.PersistentFlags().BoolVarP(&autoApprove, "yes", "y", false, "Skip confirmation prompts (auto-approve all operations)") rootCmd.PersistentFlags().BoolVar(&agentFlag, "agent", false, "Enable agent mode (auto-detected for AI coding assistants)") - rootCmd.PersistentFlags().BoolVar(&hlpFlag, "hlp", false, "Output complete command schema as JSON (for AI agents)") // Add subcommands rootCmd.AddCommand(versionCmd) diff --git a/pkg/agenthelp/agenthelp.go b/pkg/agenthelp/agenthelp.go index ad2fe553..ed3aeb93 100644 --- a/pkg/agenthelp/agenthelp.go +++ b/pkg/agenthelp/agenthelp.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/pflag" ) -// Schema is the top-level structure returned by --hlp. +// Schema is the top-level structure returned by --help in agent mode or 'pup agent schema'. type Schema struct { Version string `json:"version"` Description string `json:"description"` diff --git a/pkg/agenthelp/guide.md b/pkg/agenthelp/guide.md index 411b293d..af31eaa9 100644 --- a/pkg/agenthelp/guide.md +++ b/pkg/agenthelp/guide.md @@ -11,11 +11,11 @@ pup auth login # Or use API keys export DD_API_KEY="your-key" DD_APP_KEY="your-key" DD_SITE="datadoghq.com" -# Get the full command schema (recommended first step) -pup --hlp +# Get the full command schema (recommended first step for agents) +pup --help # Get schema for a specific domain -pup logs --hlp +pup logs --help ``` ## Authentication @@ -307,7 +307,7 @@ Agent mode is auto-detected when running inside AI coding assistants (Claude Cod pup --agent monitors list # Environment variable -DD_AGENT_MODE=1 pup monitors list +FORCE_AGENT_MODE=1 pup monitors list # Auto-detected from: CLAUDECODE, CLAUDE_CODE, CURSOR_AGENT, CODEX, AIDER, etc. ``` diff --git a/pkg/useragent/useragent.go b/pkg/useragent/useragent.go index 29acc240..34d9f5cd 100644 --- a/pkg/useragent/useragent.go +++ b/pkg/useragent/useragent.go @@ -66,9 +66,9 @@ func Get() string { return base + ")" } -// IsAgentMode returns true if any AI agent is detected or DD_AGENT_MODE=1 is set. +// IsAgentMode returns true if any AI agent is detected or FORCE_AGENT_MODE=1 is set. func IsAgentMode() bool { - if isEnvTruthy("DD_AGENT_MODE") { + if isEnvTruthy("FORCE_AGENT_MODE") { return true } return detectAgent() != "" diff --git a/pkg/useragent/useragent_test.go b/pkg/useragent/useragent_test.go index 87fe3bd9..ef268726 100644 --- a/pkg/useragent/useragent_test.go +++ b/pkg/useragent/useragent_test.go @@ -14,9 +14,9 @@ import ( "github.com/DataDog/pup/internal/version" ) -// allAgentEnvVars returns all env vars used by agent detectors plus DD_AGENT_MODE. +// allAgentEnvVars returns all env vars used by agent detectors plus FORCE_AGENT_MODE. func allAgentEnvVars() []string { - vars := []string{"DD_AGENT_MODE"} + vars := []string{"FORCE_AGENT_MODE"} for _, d := range agentDetectors { vars = append(vars, d.EnvVars...) } @@ -155,9 +155,9 @@ func TestIsAgentMode(t *testing.T) { want bool }{ {"no env", "", "", false}, - {"DD_AGENT_MODE=1", "DD_AGENT_MODE", "1", true}, - {"DD_AGENT_MODE=true", "DD_AGENT_MODE", "true", true}, - {"DD_AGENT_MODE=false", "DD_AGENT_MODE", "false", false}, + {"FORCE_AGENT_MODE=1", "FORCE_AGENT_MODE", "1", true}, + {"FORCE_AGENT_MODE=true", "FORCE_AGENT_MODE", "true", true}, + {"FORCE_AGENT_MODE=false", "FORCE_AGENT_MODE", "false", false}, {"CLAUDECODE=1", "CLAUDECODE", "1", true}, {"CURSOR_AGENT=1", "CURSOR_AGENT", "1", true}, {"AIDER=1", "AIDER", "1", true}, From 6cfb9613dbeca1ca144fa1c7928eadae7a9ed93c Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Mon, 16 Feb 2026 18:40:18 -0600 Subject: [PATCH 6/6] docs: rewrite LLM_GUIDE.md for agent operability, update ARCHITECTURE.md Rewrites LLM_GUIDE.md to document the full agent operability system: agent mode detection, --help JSON schema, pup agent commands, output envelope, structured errors, query syntax, workflows, and architecture reference with file map. Updates ARCHITECTURE.md user agent section to reflect 11 agent detectors and agent mode behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/ARCHITECTURE.md | 28 +- docs/LLM_GUIDE.md | 603 ++++++++++++++++++------------------------- 2 files changed, 268 insertions(+), 363 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 649d89d1..dd6daac2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -137,22 +137,34 @@ Based on [RFC 6749](https://tools.ietf.org/html/rfc6749) and [RFC 7636](https:// **Fallback:** - If refresh fails, prompt user to re-authenticate -## User Agent +## User Agent & Agent Mode Custom user agent identifies pup CLI and detects AI coding assistants: **Format:** ``` -pup/v0.1.0 (go go1.25.0; os darwin; arch arm64) # Without agent -pup/v0.1.0 (go go1.25.0; os darwin; arch arm64) claude-code # With agent +pup/v0.1.0 (go go1.25.0; os darwin; arch arm64) # Without agent +pup/v0.1.0 (go go1.25.0; os darwin; arch arm64; ai-agent claude-code) # With agent ``` -**AI Agent Detection:** -- `CLAUDECODE=1` or `CLAUDE_CODE=1` → appends `claude-code` -- `CURSOR_AGENT=true` or `CURSOR_AGENT=1` → appends `cursor` -- Precedence: CLAUDECODE > CURSOR_AGENT +**AI Agent Detection** (`pkg/useragent`): -**Implementation:** `pkg/useragent` - Separate package for reusability and testing. +Table-driven registry detecting 11 agents via environment variables. First match wins: +- Claude Code (`CLAUDECODE`, `CLAUDE_CODE`), Cursor (`CURSOR_AGENT`), Codex (`CODEX`, `OPENAI_CODEX`), OpenCode (`OPENCODE`), Aider (`AIDER`), Cline (`CLINE`), Windsurf (`WINDSURF_AGENT`), GitHub Copilot (`GITHUB_COPILOT`), Amazon Q (`AMAZON_Q`, `AWS_Q_DEVELOPER`), Gemini Code Assist (`GEMINI_CODE_ASSIST`), Sourcegraph Cody (`SRC_CODY`) +- Manual override: `FORCE_AGENT_MODE=1` or `--agent` flag + +**Agent Mode Behavior** (when detected): +- `--help` returns structured JSON schema instead of Cobra text +- Confirmation prompts auto-approved (prevents stdin hangs) +- API responses wrapped in metadata envelope (count, truncation, warnings) +- Errors returned as structured JSON with suggestions + +**Agent Operability Packages:** +- `pkg/agenthelp/` — Schema generation (Cobra tree walker), steering content, embedded guide +- `pkg/formatter/envelope.go` — Agent envelope and structured error formatting +- `cmd/agent.go` — `pup agent schema`, `pup agent guide` commands + +See [LLM_GUIDE.md](docs/LLM_GUIDE.md) for the complete agent guide. ## API Client Wrapper diff --git a/docs/LLM_GUIDE.md b/docs/LLM_GUIDE.md index 4c07efbf..1ee58f31 100644 --- a/docs/LLM_GUIDE.md +++ b/docs/LLM_GUIDE.md @@ -1,462 +1,355 @@ # LLM Agent Guide for Pup CLI -This guide helps AI agents (LLMs) understand and effectively use the Pup CLI tool. +This guide helps AI coding agents understand and effectively use the Pup CLI tool. It covers the agent operability system, discovery commands, query syntax, and common workflows. -## Quick Reference +For the runtime version of this guide (embedded in the binary), run `pup agent guide`. -### Discovery Commands +## Agent Mode -```bash -# See all available commands -pup --help - -# Get detailed help for any command -pup --help -pup --help +Pup auto-detects AI coding agents and switches to **agent mode**, which changes how the CLI behaves. Agent mode is triggered by any of: -# Examples -pup monitors --help -pup monitors list --help -pup auth login --help -``` - -### Authentication +| Method | Example | +|--------|---------| +| Auto-detect | `CLAUDECODE=1`, `CLAUDE_CODE=1`, `CURSOR_AGENT=1`, `CODEX=1`, `OPENAI_CODEX=1`, `OPENCODE=1`, `AIDER=1`, `CLINE=1`, `WINDSURF_AGENT=1`, `GITHUB_COPILOT=1`, `AMAZON_Q=1`, `AWS_Q_DEVELOPER=1`, `GEMINI_CODE_ASSIST=1`, `SRC_CODY=1` | +| Explicit flag | `pup --agent ` | +| Environment override | `FORCE_AGENT_MODE=1` | -```bash -# OAuth2 (Recommended) -pup auth login # Browser-based login -pup auth status # Check auth status -pup auth refresh # Refresh token -pup auth logout # Logout - -# API Keys (Legacy) -export DD_API_KEY="..." -export DD_APP_KEY="..." -``` +### What changes in agent mode -## Command Patterns +| Behavior | Human Mode | Agent Mode | +|----------|-----------|------------| +| `--help` output | Cobra text help | Structured JSON schema | +| Confirmation prompts | Interactive stdin | Auto-approved (no hangs) | +| Error format | Human text with suggestions | Structured JSON with error codes | +| API response wrapping | Raw API response | Envelope with metadata (count, truncation, warnings) | -### List Resources +### Verifying agent mode ```bash -# General pattern -pup list [--filters] +# This should return JSON schema (not text) when agent is detected +pup --help -# Examples -pup monitors list -pup monitors list --name="CPU" -pup monitors list --tags="env:production" +# Force agent mode for testing +FORCE_AGENT_MODE=1 pup --help -pup dashboards list -pup slos list -pup incidents list +# Subtree schema (only logs commands + logs query syntax) +FORCE_AGENT_MODE=1 pup logs --help ``` -### Get Resource Details +## Discovery Commands (Recommended First Steps) -```bash -# General pattern -pup get - -# Examples -pup monitors get 12345678 -pup dashboards get abc-def-123 -pup slos get abc-123-def -pup incidents get abc-123-def -``` +### 1. Get full command schema -### Delete Resources +In agent mode, `--help` returns the complete JSON schema with all commands, flags, query syntax, workflows, best practices, and anti-patterns in a single call: ```bash -# General pattern (requires confirmation) -pup delete - -# With auto-approve (no confirmation) -pup delete --yes - -# Examples -pup monitors delete 12345678 --yes -pup dashboards delete abc-def-123 --yes -pup slos delete abc-123-def --yes +pup --help +# Returns: { version, auth, global_flags, commands[], query_syntax, time_formats, workflows, best_practices, anti_patterns } ``` -## Parsing Output - -All commands output JSON by default. Use `jq` for parsing: +### 2. Get domain-specific schema ```bash -# Get monitor names -pup monitors list | jq '.[] | .name' - -# Filter by field -pup monitors list | jq '.[] | select(.overall_state == "Alert")' - -# Extract specific fields -pup monitors get 12345678 | jq '{name: .name, state: .overall_state}' - -# Count resources -pup dashboards list | jq '.dashboards | length' +pup logs --help # Only logs commands + logs query syntax +pup monitors --help # Only monitors commands +pup metrics --help # Only metrics commands ``` -## Common Tasks - -### Monitor Management +### 3. Explicit schema commands (work regardless of agent mode) ```bash -# Find all critical monitors -pup monitors list | jq '.[] | select(.overall_state == "Alert")' - -# Get monitor by name pattern -pup monitors list --name="CPU" | jq '.[0] | .id' - -# Check monitor status -pup monitors get 12345678 | jq '.overall_state' - -# List production monitors -pup monitors list --tags="env:production" +pup agent schema # Full JSON schema +pup agent schema --compact # Minimal schema (names + flags only, fewer tokens) +pup agent guide # Full steering guide (markdown) +pup agent guide logs # Domain-specific guide section ``` -### Dashboard Management +## Authentication ```bash -# Find dashboard by name -pup dashboards list | jq '.dashboards[] | select(.title | contains("API"))' - -# Get dashboard ID -pup dashboards list | jq '.dashboards[0] | .id' +# OAuth2 (recommended) — opens browser for secure login +pup auth login -# Backup dashboard -pup dashboards get abc-def-123 > dashboard-backup.json +# Check auth status +pup auth status -# Export all dashboards -pup dashboards list | jq -r '.dashboards[].id' | \ - xargs -I {} pup dashboards get {} > dashboards-{}.json +# API keys (legacy) — set environment variables +export DD_API_KEY="your-key" +export DD_APP_KEY="your-key" +export DD_SITE="datadoghq.com" ``` -### SLO Monitoring +- OAuth2 tokens are stored in the OS keychain and refresh automatically +- Some endpoints require API keys even with OAuth2 (e.g., logs search v1) +- In agent mode, if auth fails, the error JSON includes `suggestions` with remediation steps -```bash -# Find breaching SLOs -pup slos list | jq '.data[] | select(.status.state == "breaching")' - -# Check error budget -pup slos get abc-123 | jq '.data.error_budget_remaining' +## Command Patterns -# List all SLO statuses -pup slos list | jq '.data[] | {name: .name, state: .status.state, budget: .status.error_budget_remaining}' -``` +All commands follow `pup [flags]` or `pup [flags]`. -### Incident Response +### CRUD operations ```bash -# Find active incidents -pup incidents list | jq '.data[] | select(.state == "active")' - -# Find SEV-1 incidents -pup incidents list | jq '.data[] | select(.severity == "SEV-1")' - -# Get incident timeline -pup incidents get abc-123 | jq '.data.timeline' - -# Check customer impact -pup incidents list | jq '.data[] | select(.customer_impacted == true)' +pup list [--filters] # List/search resources +pup get # Get details by ID +pup delete [--yes] # Delete (--yes to skip confirmation) +pup create [--body=file.json] # Create from JSON +pup update [--body=...] # Update resource ``` -## Help Text Structure - -Each command provides structured help with these sections: - -1. **CAPABILITIES**: What the command can do -2. **EXAMPLES**: Real-world usage examples -3. **OUTPUT FIELDS**: Description of output structure -4. **FILTERS**: Available filtering options -5. **AUTHENTICATION**: Auth requirements - -### Example Help Output +### Output formats ```bash -$ pup monitors list --help - -FILTERS: - --name Filter by monitor name (substring match) - --tags Filter by tags (comma-separated) - -EXAMPLES: - pup monitors list - pup monitors list --name="CPU" - pup monitors list --tags="env:production" - -OUTPUT FIELDS: - • id: Monitor ID - • name: Monitor name - • type: Monitor type - • query: Monitor query - • overall_state: Current state +pup monitors list --output=json # JSON (default, recommended for agents) +pup monitors list --output=table # Human-readable table +pup monitors list --output=yaml # YAML ``` -## Error Handling - -```bash -# Commands return non-zero exit codes on error -pup monitors get 99999999 -echo $? # Non-zero +## Query Syntax by Domain -# Capture errors -if ! pup monitors get 99999999 2>&1; then - echo "Monitor not found" -fi +### Logs -# Parse error messages -pup monitors get 99999999 2>&1 | grep "Error" ``` - -## Automation Patterns - -### Confirmation Bypass - -```bash -# Method 1: --yes flag -pup monitors delete 12345678 --yes - -# Method 2: Environment variable -DD_AUTO_APPROVE=true pup monitors delete 12345678 - -# Method 3: Global flag -pup monitors delete 12345678 -y +status:error # Filter by status +service:web-app # Filter by service +@user.id:12345 # Custom attribute (@ prefix) +host:i-* # Wildcard matching +"exact error message" # Exact phrase matching +status:error AND service:web # Boolean AND (implicit or explicit) +status:error OR status:warn # Boolean OR +-status:info # Negation +@http.status_code:[400 TO 599] # Numeric range ``` -### Multi-Site Operations - ```bash -# Operate on different sites -DD_SITE=datadoghq.com pup monitors list -DD_SITE=datadoghq.eu pup monitors list -DD_SITE=us3.datadoghq.com pup monitors list -``` +# Search logs +pup logs search --query="status:error AND service:api" --from=1h --limit=100 -### Batch Operations +# Aggregate logs (counting, statistics) +pup logs aggregate --query="*" --from=1h --compute="count" --group-by="service" -```bash -# Delete multiple monitors -for id in 111 222 333; do - pup monitors delete $id --yes -done - -# Backup all dashboards -pup dashboards list | jq -r '.dashboards[].id' | while read id; do - pup dashboards get "$id" > "dashboard-$id.json" -done +# Storage tiers +pup logs search --query="*" --from=30d --storage="flex" ``` -## LLM-Specific Tips +### Metrics -### 1. Always Check Help First - -Before using any command, check its help text: - -```bash -pup --help -pup --help ``` +:{} by {} -### 2. Use Structured Output - -All commands output JSON. Parse with jq: - -```bash -pup | jq '.' +avg:system.cpu.user{env:prod} by {host} # CPU by host +sum:trace.servlet.request.hits{service:web} # Request count +max:system.mem.used{*} by {host} # Max memory ``` -### 3. Understand Authentication - -Check if authenticated before running commands: - ```bash -pup auth status +pup metrics query --query="avg:system.cpu.user{env:prod} by {host}" --from=1h +pup metrics list --query="system.cpu" ``` -### 4. Filter with jq, Not Grep +### APM / Traces -Use jq for structured filtering: +**CRITICAL: Durations are in NANOSECONDS** +- 1ms = 1,000,000 ns +- 1s = 1,000,000,000 ns -```bash -# Good -pup monitors list | jq '.[] | select(.name | contains("CPU"))' - -# Less good -pup monitors list | grep "CPU" ``` - -### 5. Save Output for Analysis - -Save command output to files for later analysis: +service: # Filter by service +resource_name: # Filter by endpoint +@duration:>5000000000 # Duration > 5s (nanoseconds!) +status:error # Error spans only +env:production # Filter by environment +``` ```bash -pup monitors list > monitors.json -jq '.[] | select(.overall_state == "Alert")' monitors.json +pup traces search --query="service:api AND @duration:>1000000000" --from=1h +pup apm services list ``` -## Resource Types - ### Monitors -- **ID Format**: Numeric (e.g., 12345678) -- **List Command**: `pup monitors list` -- **Get Command**: `pup monitors get ` -- **Filter By**: name, tags - -### Dashboards -- **ID Format**: UUID-like (e.g., abc-def-123) -- **List Command**: `pup dashboards list` -- **Get Command**: `pup dashboards get ` -- **Filter By**: None (use jq) - -### SLOs -- **ID Format**: UUID-like (e.g., abc-123-def) -- **List Command**: `pup slos list` -- **Get Command**: `pup slos get ` -- **Filter By**: None (use jq) - -### Incidents -- **ID Format**: UUID-like (e.g., abc-123-def) -- **List Command**: `pup incidents list` -- **Get Command**: `pup incidents get ` -- **Filter By**: None (use jq) - -## Output Format - -### JSON (Default) ```bash -pup monitors list -# Returns: JSON array or object +pup monitors list --tags="env:production" --name="CPU" # Filter by tags/name +pup monitors search --query="status:Alert" # Full-text search +pup monitors get 12345678 # Get by ID ``` -### Table (Future) +### RUM -```bash -pup monitors list --output=table -# Returns: Formatted table ``` - -### YAML (Future) - -```bash -pup monitors list --output=yaml -# Returns: YAML format +@type:error # Error events +@type:view # Page views +@view.loading_time:>3000 # Slow pages (milliseconds) +@session.type:user # Real users (not synthetic) ``` -## Troubleshooting - -### Authentication Issues +### Incidents ```bash -# Check auth status -pup auth status - -# Re-authenticate -pup auth login - -# Check API keys (legacy) -echo $DD_API_KEY -echo $DD_APP_KEY +pup incidents list --query="status:active" +pup incidents get ``` -### Rate Limiting +## Time Ranges -If you encounter rate limits: -- Reduce request frequency -- Use filters to limit data retrieved -- Cache responses when possible +All `--from` and `--to` flags accept: -### Invalid IDs - -```bash -# Verify resource exists -pup monitors list | jq '.[] | .id' - -# Then get specific resource -pup monitors get -``` - -## Best Practices +| Format | Example | +|--------|---------| +| Relative short | `1h`, `30m`, `7d`, `5s`, `1w` | +| Relative long | `5min`, `2hours`, `3days` | +| With spaces | `"5 minutes"`, `"2 hours"` | +| RFC3339 | `2024-01-01T00:00:00Z` | +| Unix ms | `1704067200000` | +| Keyword | `now` | -1. **Always authenticate first** - ```bash - pup auth login - ``` +## Common Workflows -2. **Use filters to reduce data** - ```bash - pup monitors list --tags="env:production" - ``` +### Error investigation -3. **Save responses for reuse** - ```bash - pup monitors list > monitors.json - ``` - -4. **Check help for each command** - ```bash - pup --help - ``` +```bash +# 1. Get error counts by service +pup logs aggregate --query="status:error" --from=1h --compute="count" --group-by="service" -5. **Use jq for parsing** - ```bash - pup monitors list | jq '.[] | .name' - ``` +# 2. Drill into affected service +pup logs search --query="status:error AND service:" --from=1h --limit=20 -6. **Auto-approve for automation** - ```bash - pup monitors delete --yes - ``` +# 3. Check monitors for that service +pup monitors list --tags="service:" -## Example Workflows +# 4. Check recent events +pup events list --from=4h +``` -### Morning Health Check +### Performance investigation ```bash -# Check authentication -pup auth status - -# Check for alerts -pup monitors list | jq '.[] | select(.overall_state == "Alert")' +# 1. Check service latency +pup metrics query --query="avg:trace.servlet.request.duration{service:} by {resource_name}" --from=1h -# Check active incidents -pup incidents list | jq '.data[] | select(.state == "active")' +# 2. Find slow traces (>5 seconds) +pup traces search --query="service: AND @duration:>5000000000" --from=1h -# Check SLO breaches -pup slos list | jq '.data[] | select(.status.state == "breaching")' +# 3. Check resource utilization +pup metrics query --query="avg:system.cpu.user{service:} by {host}" --from=1h ``` -### Dashboard Backup +### Service health overview ```bash -# List all dashboards -pup dashboards list > dashboard-list.json - -# Backup each dashboard -cat dashboard-list.json | jq -r '.dashboards[].id' | while read id; do - pup dashboards get "$id" > "backups/dashboard-$id.json" - echo "Backed up dashboard $id" -done +pup slos list +pup monitors list --tags="team:" +pup incidents list --query="status:active" ``` -### Monitor Audit - -```bash -# Find untagged monitors -pup monitors list | jq '.[] | select(.tags | length == 0)' - -# Find monitors without notifications -pup monitors list | jq '.[] | select(.message | contains("@") | not)' +## Agent Envelope (Agent Mode Output) + +In agent mode, command output is wrapped in a metadata envelope: + +```json +{ + "status": "success", + "data": [ ... ], + "metadata": { + "count": 42, + "truncated": false, + "command": "monitors list", + "warnings": [] + } +} +``` -# Find monitors in Alert state -pup monitors list | jq '.[] | select(.overall_state == "Alert")' +Error responses in agent mode: + +```json +{ + "status": "error", + "error_code": 401, + "error_message": "Authentication failed", + "operation": "list monitors", + "suggestions": [ + "Run 'pup auth login' to re-authenticate", + "Or set DD_API_KEY and DD_APP_KEY environment variables" + ] +} ``` -## Additional Resources +## Best Practices -- **Main Docs**: README.md -- **OAuth2 Guide**: docs/OAUTH2.md -- **Developer Guide**: CLAUDE.md -- **Implementation**: SUMMARY.md +1. **Always specify `--from`** — most commands default to 1h but be explicit +2. **Start narrow, widen later** — begin with 1h, expand to 24h/7d only if needed +3. **Filter at the API level** — use `--tags`, `--query`, `--name` instead of fetching everything and parsing locally +4. **Use `aggregate` for counts** — don't fetch all logs and count them yourself +5. **APM durations are in nanoseconds** — 1s = 1,000,000,000 +6. **Use `--yes` for automation** — or rely on agent mode auto-approval +7. **Check `pup agent schema`** when unsure about a command's flags +8. **Chain queries** — aggregate first to find patterns, then search for specifics + +## Anti-Patterns + +1. **Don't omit `--from`** on time-series queries — you'll get unexpected ranges or errors +2. **Don't use `--limit=1000` as a first step** — start small and refine +3. **Don't list all monitors without filters** in large orgs (>10k monitors) +4. **Don't assume durations are in seconds** — APM uses nanoseconds +5. **Don't fetch raw logs to count them** — use `pup logs aggregate --compute=count` +6. **Don't retry 401/403 errors** — re-authenticate or check permissions instead +7. **Don't use `--from=30d`** unless you specifically need a month of data + +## Error Reference + +| Status | Meaning | Suggested Action | +|--------|---------|------------------| +| 401 | Authentication failed | `pup auth login` or check DD_API_KEY/DD_APP_KEY | +| 403 | Insufficient permissions | Verify API/App key scopes | +| 404 | Resource not found | Check the resource ID | +| 429 | Rate limited | Wait and retry with backoff | +| 5xx | Server error | Retry after a short delay; check https://status.datadoghq.com/ | + +## Architecture Reference + +### Agent detection + +- Implementation: `pkg/useragent/useragent.go` +- Table-driven detector registry; first match wins +- `IsAgentMode()` checks `FORCE_AGENT_MODE` first, then agent env vars +- `DetectAgentInfo()` returns `AgentInfo{Name, Detected}` + +### Schema generation + +- Implementation: `pkg/agenthelp/agenthelp.go` +- Walks the Cobra command tree via `rootCmd.Commands()` recursion +- Schema stays in sync automatically as commands are added +- Subtree schemas filter to a single domain + relevant query syntax + +### Output envelope + +- Implementation: `pkg/formatter/envelope.go` +- `WrapForAgent(data, metadata)` wraps responses in `AgentEnvelope` +- `FormatAgentError(operation, statusCode, message, apiBody)` formats structured errors +- Only activated when `cfg.AgentMode == true` + +### Steering content + +- Query syntax, time formats, workflows, best practices, anti-patterns: `pkg/agenthelp/steering.go` +- Embedded guide document: `pkg/agenthelp/guide.md` (loaded via `go:embed`) +- Guide sections retrievable by domain: `agenthelp.GetGuideSection("logs")` + +### Help interception + +- `cmd/root.go:ExecuteWithArgs()` intercepts `--help`/`-h` before Cobra processes args +- When `useragent.IsAgentMode()` is true, calls `printAgentSchema()` instead of Cobra help +- `firstNonFlagArg()` extracts the domain name for subtree schemas + +## File Map + +| File | Purpose | +|------|---------| +| `pkg/useragent/useragent.go` | Agent detection (11 agents + FORCE_AGENT_MODE) | +| `pkg/agenthelp/agenthelp.go` | Schema generation (Cobra tree walker) | +| `pkg/agenthelp/steering.go` | Query syntax, workflows, best practices | +| `pkg/agenthelp/guide.go` | Embedded guide document (`go:embed`) | +| `pkg/agenthelp/guide.md` | Runtime steering guide content | +| `pkg/formatter/envelope.go` | Agent envelope and structured errors | +| `pkg/config/config.go` | `AgentMode` field on Config | +| `cmd/root.go` | `--agent` flag, help interception, `formatAndPrint()`, `formatAPIError()` | +| `cmd/agent.go` | `pup agent schema`, `pup agent guide` commands |