diff --git a/README.md b/README.md index 81c14c6..25e8534 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,43 @@ dci list-budgets --table-mode wrap dci list-budgets --table-columns id,name,amount ``` +## Agent Mode + +`dci` adapts its output depending on whether a human or an AI agent is driving it. +In **human mode** (the default in an interactive terminal) you get tables, color, +and contextual hints. In **agent mode** the CLI emits clean, deterministic output +that is cheap to parse and free of decoration: + +- Default `--output` becomes `toon` (compact, token-efficient) instead of `table` +- No color, spinners, or other terminal decoration +- Banners, tips, and status chatter go to **stderr**, leaving **stdout** for data only +- The request `User-Agent` carries a `mode=` token — `agent` (explicit flag/env or a known AI-agent environment), `noninteractive` (piped/redirected output or CI/CD), or `interactive` (human at a terminal) — so API traffic can be segmented by interface + +### How agent mode is detected + +Detection runs in priority order — the first match wins: + +1. **`DCI_AGENT_MODE` environment variable** — `DCI_AGENT_MODE=1` forces agent mode, + `DCI_AGENT_MODE=0` forces human mode. Always wins. +2. **`--agent` / `--no-agent` flags** — explicit, per-invocation override. +3. **Known agent environment variables** — if any of these are set, agent mode is + assumed: `CLAUDECODE`, `CLAUDE_CODE`, `CURSOR_AGENT`, `KIRO_AGENT`, + `AIDER_SESSION`, `GEMINI_CLI`, `REPLIT_AGENT`, `WINDSURF_AGENT`, + `OPENHANDS_AGENT`, `DEVIN_AGENT`. (Open a PR to extend this list.) +4. **Non-TTY stdout** — a soft signal: when output is piped or redirected and no + setting above applies, agent mode is assumed. + +```bash +# Force agent mode +DCI_AGENT_MODE=1 dci list-budgets +dci --agent list-budgets + +# Force human mode (tables, color) even when piping or running under an agent +dci --no-agent list-budgets | less -S +``` + +Run `dci status` to see whether agent mode is active and why. + ## Updating ```bash diff --git a/main.go b/main.go index dedc0cd..796a209 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,34 @@ var skillFS embed.FS // set, used to suppress the Doer hint even when no persistent context file exists. var customerContextFlagValue string +// agentMode reports whether dci is running in agent mode for the current +// invocation (compact, deterministic output with no decoration). It is resolved +// once at startup and read throughout the run. +var agentMode bool + +// agentModeReason records why agentMode resolved the way it did, for diagnostics. +var agentModeReason string + +// uaMode classifies how dci is being driven, for the User-Agent "mode=" token. +// It is finer-grained than agentMode (a bool): agent mode reached via the +// non-TTY soft signal (pipes, redirects, CI/CD) is reported as noninteractive +// rather than agent, so analytics can tell genuine AI-agent traffic apart from +// incidental non-interactive use. +type uaMode string + +const ( + uaModeInteractive uaMode = "interactive" // human at a TTY (or explicit human mode) + uaModeAgent uaMode = "agent" // explicit --agent/DCI_AGENT_MODE=1, or a known AI-agent env var + uaModeNonInteractive uaMode = "noninteractive" // non-TTY soft signal: pipe/redirect/CI +) + +// agentUAMode records the interface classification for the User-Agent token. +var agentUAMode uaMode + +// agentEnvDetected holds the name of the detected agent environment variable (if +// any), used to surface the human-mode "an optimized agent mode exists" tip. +var agentEnvDetected string + func dciConfigDir() string { if dir, err := os.UserConfigDir(); err == nil && dir != "" { cfgDir := filepath.Join(dir, "dci") @@ -172,7 +200,8 @@ func tightenFilePermissions(path string, desired os.FileMode) error { } func printFirstRunOnboarding(configured bool) { - if !configured || !term.IsTerminal(int(os.Stderr.Fd())) { + // Onboarding is decorative chatter — skip it entirely in agent mode. + if !configured || agentMode || !term.IsTerminal(int(os.Stderr.Fd())) { return } @@ -193,6 +222,25 @@ func run() (exitCode int) { // Reset per-invocation state so repeated calls (e.g. in tests) start clean. customerContextFlagValue = "" + // Resolve agent mode once up front. Downstream behavior — color, default + // output format, stderr routing, and the User-Agent mode token — all key off + // this. + agentEnvDetected = detectedAgentEnv() + if v := strings.TrimSpace(os.Getenv("DCI_AGENT_MODE")); v != "" { + if _, ok := parseBoolish(v); !ok { + fmt.Fprintf(os.Stderr, "warning: ignoring unrecognized DCI_AGENT_MODE=%q (use 1 or 0)\n", v) + } + } + dec := resolveAgentMode(os.Getenv("DCI_AGENT_MODE"), os.Args, agentEnvDetected, stdoutIsTTY()) + agentMode = dec.enabled + agentModeReason = dec.reason + agentUAMode = dec.mode + if agentMode { + // Disable restish/aurora color before cli.Init configures the output + // writers; it reads this via viper's automatic env binding. + os.Setenv("NOCOLOR", "1") + } + defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "dci encountered an internal error: %v\n", r) @@ -213,7 +261,9 @@ func run() (exitCode int) { cli.Init("dci", version) cli.Defaults() overrideTableOutput() + registerAgentFlags() printFirstRunOnboarding(configured) + maybeHintAgentMode() cli.AddLoader(openapi.New()) cli.AddAuth("oauth-authorization-code", &oauth.AuthorizationCodeHandler{}) @@ -226,10 +276,11 @@ func run() (exitCode int) { os.Setenv("RSH_PROFILE", "default") viper.Set("rsh-profile", "default") - // Hardcode user-agent so the DCI API can identify CLI traffic. - // Restish picks this up via rsh-header and skips its own default. - userAgent := fmt.Sprintf("dci-cli/%s (%s; %s/%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) - viper.Set("rsh-header", []string{"user-agent:" + userAgent}) + // Hardcode user-agent so the DCI API can identify CLI traffic. It carries a + // mode= token (never the end user) so + // traffic can be segmented by interface. Restish picks this up via rsh-header + // and skips its own default. + viper.Set("rsh-header", []string{"user-agent:" + buildUserAgent(agentUAMode)}) cli.Load("dci", cli.Root) applyAPIKeyAuth() @@ -448,12 +499,209 @@ func isRootCommand(name string) bool { } func hideGlobalFlags() { - // Keep the flags functional but hide them from help output. + // Keep the flags functional but hide them from help output. The agent-mode + // flags stay visible so users can discover them. cli.Root.PersistentFlags().VisitAll(func(f *pflag.Flag) { + if f.Name == "agent" || f.Name == "no-agent" { + return + } f.Hidden = true }) } +// --- Agent mode detection ------------------------------------------------- + +// agentEnvVars lists environment variables set by known AI coding agents. When +// any is present with a non-empty value, dci treats the session as agent-driven +// unless an explicit override says otherwise. The list is intentionally +// conservative and documented; PRs to extend it are welcome. +var agentEnvVars = []string{ + "CLAUDECODE", // Claude Code + "CLAUDE_CODE", // Claude Code (defensive alias) + "CURSOR_AGENT", // Cursor agent + "KIRO_AGENT", // Kiro + "AIDER_SESSION", // Aider + "GEMINI_CLI", // Gemini CLI + "REPLIT_AGENT", // Replit Agent + "WINDSURF_AGENT", // Windsurf + "OPENHANDS_AGENT", // OpenHands + "DEVIN_AGENT", // Devin +} + +type agentModeResult struct { + enabled bool + mode uaMode + reason string +} + +// resolveAgentMode decides whether agent mode is active, following the +// documented precedence: +// 1. DCI_AGENT_MODE env var (explicit override, always wins) +// 2. --agent / --no-agent flags (explicit, per-invocation) +// 3. a known agent env var (heuristic) +// 4. non-TTY stdout (soft signal: pipe/redirect) +// +// Inputs are passed explicitly so the logic stays easy to test. +func resolveAgentMode(dciAgentMode string, args []string, agentEnv string, stdoutTTY bool) agentModeResult { + if v := strings.TrimSpace(dciAgentMode); v != "" { + // Only recognized boolean tokens are decisive. An unrecognized value + // (e.g. DCI_AGENT_MODE=2) is ignored so a typo can't silently force a + // mode; run() warns about it separately. + if b, ok := parseBoolish(v); ok { + if b { + return agentModeResult{enabled: true, mode: uaModeAgent, reason: "DCI_AGENT_MODE override"} + } + return agentModeResult{enabled: false, mode: uaModeInteractive, reason: "DCI_AGENT_MODE override"} + } + } + switch agentFlagOverride(args) { + case 1: + return agentModeResult{enabled: true, mode: uaModeAgent, reason: "--agent/--no-agent flag"} + case -1: + return agentModeResult{enabled: false, mode: uaModeInteractive, reason: "--agent/--no-agent flag"} + } + if agentEnv != "" { + return agentModeResult{enabled: true, mode: uaModeAgent, reason: "agent env var " + agentEnv} + } + if !stdoutTTY { + // Non-TTY is a soft signal (pipe/redirect/CI), not a confirmed agent, so + // it gets its own UA classification even though behavior matches agent mode. + return agentModeResult{enabled: true, mode: uaModeNonInteractive, reason: "non-TTY stdout"} + } + return agentModeResult{enabled: false, mode: uaModeInteractive, reason: "interactive terminal"} +} + +// detectedAgentEnv returns the name of the first agent env var found, or "". +func detectedAgentEnv() string { + for _, name := range agentEnvVars { + if strings.TrimSpace(os.Getenv(name)) != "" { + return name + } + } + return "" +} + +func stdoutIsTTY() bool { + return term.IsTerminal(int(os.Stdout.Fd())) +} + +// agentFlagOverride scans args for the explicit --agent / --no-agent flags and +// returns +1 for agent mode, -1 for human mode, or 0 for no override. These are +// two independent pflag bool flags, so this scan mirrors pflag's own semantics: +// each flag's last occurrence wins, a bare flag means true, and the value (when +// present) is parsed with strconv.ParseBool exactly as pflag does. Crucially, an +// explicit false (--agent=false / --no-agent=0) just leaves that flag unset +// rather than forcing the opposite mode — only a flag set to true is an +// override. If both end up true, the most recently enabled one wins. Scanning +// stops at the "--" operand terminator. Keeping this in step with pflag matters +// because the result drives side effects (color, default output, User-Agent) +// before cobra parses the flags itself. +func agentFlagOverride(args []string) int { + agent, noAgent := false, false + last := 0 // +1 if --agent was most recently enabled, -1 if --no-agent was + for _, a := range args { + if a == "--" { + break + } + name, val, hasVal := strings.Cut(a, "=") + if name != "--agent" && name != "--no-agent" { + continue + } + enabled := true + if hasVal { + b, err := strconv.ParseBool(val) + if err != nil { + continue // let cobra surface the parse error later + } + enabled = b + } + switch name { + case "--agent": + agent = enabled + case "--no-agent": + noAgent = enabled + } + if enabled { + if name == "--agent" { + last = 1 + } else { + last = -1 + } + } + } + switch { + case agent && noAgent: + return last // conflicting flags: most recently enabled wins + case agent: + return 1 + case noAgent: + return -1 + default: + return 0 + } +} + +// parseBoolish interprets a boolean-ish env var value (DCI_AGENT_MODE), +// case-insensitively and forgivingly: it accepts the strconv.ParseBool tokens +// plus common aliases (yes/no, on/off, y/n). Returns (value, true) for a +// recognized token and (false, false) otherwise. It is intentionally more +// lenient than the flag parsing, which mirrors pflag exactly. +func parseBoolish(v string) (bool, bool) { + switch strings.ToLower(strings.TrimSpace(v)) { + case "1", "t", "true", "yes", "y", "on": + return true, true + case "0", "f", "false", "no", "n", "off": + return false, true + } + return false, false +} + +// buildUserAgent returns the User-Agent header value identifying CLI traffic to +// the DCI API. It always carries a mode= +// token so API traffic can be segmented by interface in analytics. The token +// reflects only how dci was driven, never the end user, so the value stays a +// stable client identifier. +func buildUserAgent(mode uaMode) string { + if mode == "" { + mode = uaModeInteractive + } + return fmt.Sprintf("dci-cli/%s (%s; %s/%s; mode=%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH, mode) +} + +// defaultOutputFormat is the output format used when --output is not given. +// Humans get the table view; agents get TOON — compact, token-efficient, and +// parse-friendly. +func defaultOutputFormat() string { + if agentMode { + return "toon" + } + return "table" +} + +// registerAgentFlags adds the global --agent / --no-agent flags so cobra accepts +// them. The actual decision is made earlier in run() by scanning os.Args, since +// it must be live before any HTTP call (User-Agent) and before cli.Init (color). +func registerAgentFlags() { + pf := cli.Root.PersistentFlags() + if pf.Lookup("agent") == nil { + pf.Bool("agent", false, "Force agent mode: compact TOON, no color, chatter to stderr") + } + if pf.Lookup("no-agent") == nil { + pf.Bool("no-agent", false, "Force human mode even when an agent is auto-detected") + } +} + +// maybeHintAgentMode nudges a misclassified agent toward the optimized path: +// when an agent env var is present but agent mode is off (the caller opted out +// via --no-agent or DCI_AGENT_MODE=0), emit a one-line tip on stderr so stdout +// stays parseable. +func maybeHintAgentMode() { + if agentMode || agentEnvDetected == "" { + return + } + fmt.Fprintln(os.Stderr, "Tip: set DCI_AGENT_MODE=1 (or pass --agent) for compact, parse-friendly output.") +} + const dciUsageTemplate = `Usage:{{if .Runnable}} {{.Use}}{{if .HasAvailableFlags}} [flags]{{end}}{{end}}{{if .HasAvailableSubCommands}} dci [command] @@ -669,10 +917,16 @@ func setupCompletion() { defaultHelp(cmd, args) if cmd == cli.Root && !hasAPICommands { hint := "\n! To get started, authenticate with: dci login (or set DCI_API_KEY)\n\n" - if term.IsTerminal(int(os.Stdout.Fd())) { - hint = "\n\033[1;33m!\033[0m To get started, authenticate with: \033[1mdci login\033[0m (or set \033[1mDCI_API_KEY\033[0m)\n\n" + // In agent mode the hint is chatter — route it to stderr (plain, no + // color) so stdout stays parseable. + if agentMode { + fmt.Fprint(os.Stderr, hint) + } else { + if term.IsTerminal(int(os.Stdout.Fd())) { + hint = "\n\033[1;33m!\033[0m To get started, authenticate with: \033[1mdci login\033[0m (or set \033[1mDCI_API_KEY\033[0m)\n\n" + } + fmt.Fprint(os.Stdout, hint) } - fmt.Fprint(os.Stdout, hint) } }) } @@ -807,7 +1061,7 @@ func registerStatusCommands(configDir string) { currentOutput := func() string { output := strings.TrimSpace(viper.GetString("rsh-output-format")) if output == "" || output == "auto" { - output = "table" + output = defaultOutputFormat() } return output } @@ -828,6 +1082,11 @@ func registerStatusCommands(configDir string) { } fmt.Fprintf(os.Stdout, "Auth: %s\n", authSource()) fmt.Fprintf(os.Stdout, "Default Output: %s\n", currentOutput()) + if agentMode { + fmt.Fprintf(os.Stdout, "Agent Mode: on (%s)\n", agentModeReason) + } else { + fmt.Fprintf(os.Stdout, "Agent Mode: off (%s)\n", agentModeReason) + } fmt.Fprintf(os.Stdout, "Config Dir: %s\n", configDir) if ctx != "" { if os.Getenv("DCI_CUSTOMER_CONTEXT") != "" { @@ -1058,7 +1317,7 @@ func addOutputFlag() { return } - dciCmd.PersistentFlags().String("output", "", "Output format: table, json, yaml, auto, toon (default: table). toon is compact and token-efficient — good for LLM agents.") + dciCmd.PersistentFlags().String("output", "", "Output format: table, json, yaml, auto, toon (default: table, or toon in agent mode). toon is compact and token-efficient — good for LLM agents.") dciCmd.PersistentFlags().StringP("table-mode", "M", "fit", "Table rendering: fit (truncate) or wrap (multi-line)") dciCmd.PersistentFlags().StringP("table-columns", "C", "", "Comma-separated list of columns to include (default: all)") dciCmd.PersistentFlags().IntP("table-width", "W", 0, "Table width in columns (default: auto-detect terminal width)") @@ -1076,7 +1335,7 @@ func addOutputFlag() { outFlag := cmd.Flags().Lookup("output") if outFlag == nil || !outFlag.Changed { - viper.Set("rsh-output-format", "table") + viper.Set("rsh-output-format", defaultOutputFormat()) } else { out := strings.TrimSpace(outFlag.Value.String()) switch out { diff --git a/main_test.go b/main_test.go index 6400824..aa3e47a 100644 --- a/main_test.go +++ b/main_test.go @@ -387,6 +387,153 @@ func TestTruncateTextDisplayWidth(t *testing.T) { } } +func TestResolveAgentMode(t *testing.T) { + tests := []struct { + name string + envMode string + args []string + agentEnv string + stdoutTTY bool + want bool + wantMode uaMode + }{ + // 1. DCI_AGENT_MODE always wins. + {name: "env mode 1 wins over tty", envMode: "1", args: nil, agentEnv: "", stdoutTTY: true, want: true, wantMode: uaModeAgent}, + {name: "env mode true", envMode: "true", stdoutTTY: true, want: true, wantMode: uaModeAgent}, + {name: "env mode 0 forces human", envMode: "0", agentEnv: "CLAUDECODE", stdoutTTY: false, want: false, wantMode: uaModeInteractive}, + {name: "env mode false forces human", envMode: "false", stdoutTTY: false, want: false, wantMode: uaModeInteractive}, + {name: "env mode wins over --agent", envMode: "0", args: []string{"--agent"}, stdoutTTY: false, want: false, wantMode: uaModeInteractive}, + {name: "env mode garbage ignored, falls through to tty", envMode: "2", stdoutTTY: true, want: false, wantMode: uaModeInteractive}, + {name: "env mode garbage ignored, falls through to env var", envMode: "banana", agentEnv: "KIRO_AGENT", stdoutTTY: true, want: true, wantMode: uaModeAgent}, + + // 2. Flags override heuristics. + {name: "--agent forces agent", args: []string{"dci", "--agent", "status"}, stdoutTTY: true, want: true, wantMode: uaModeAgent}, + {name: "--no-agent forces human", args: []string{"dci", "--no-agent", "list"}, agentEnv: "CLAUDECODE", stdoutTTY: false, want: false, wantMode: uaModeInteractive}, + {name: "--no-agent over tty", args: []string{"--no-agent"}, stdoutTTY: false, want: false, wantMode: uaModeInteractive}, + + // 3. Agent env var heuristic. + {name: "agent env enables", agentEnv: "CURSOR_AGENT", stdoutTTY: true, want: true, wantMode: uaModeAgent}, + + // 4. Non-TTY soft signal — agent behavior, but classified noninteractive + // (covers CI/CD and piped/redirected use). + {name: "non-tty enables", stdoutTTY: false, want: true, wantMode: uaModeNonInteractive}, + {name: "interactive tty stays human", stdoutTTY: true, want: false, wantMode: uaModeInteractive}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveAgentMode(tt.envMode, tt.args, tt.agentEnv, tt.stdoutTTY) + if got.enabled != tt.want { + t.Fatalf("resolveAgentMode() enabled = %v (reason %q), want %v", got.enabled, got.reason, tt.want) + } + if got.mode != tt.wantMode { + t.Fatalf("resolveAgentMode() mode = %q (reason %q), want %q", got.mode, got.reason, tt.wantMode) + } + }) + } +} + +func TestAgentFlagOverride(t *testing.T) { + tests := []struct { + name string + args []string + want int + }{ + {name: "none", args: []string{"dci", "status"}, want: 0}, + {name: "agent", args: []string{"dci", "--agent", "status"}, want: 1}, + {name: "agent equals true", args: []string{"--agent=true"}, want: 1}, + {name: "agent equals 1", args: []string{"--agent=1"}, want: 1}, + {name: "no-agent", args: []string{"--no-agent"}, want: -1}, + {name: "no-agent equals 1", args: []string{"--no-agent=1"}, want: -1}, + {name: "agent uppercase TRUE", args: []string{"--agent=TRUE"}, want: 1}, + + // Explicit false leaves the flag unset (matching two independent pflag + // bool flags), so it does NOT force the opposite mode — it clears to 0. + {name: "agent equals false clears", args: []string{"--agent=false"}, want: 0}, + {name: "agent equals 0 clears", args: []string{"--agent=0"}, want: 0}, + {name: "no-agent equals false clears", args: []string{"--no-agent=false"}, want: 0}, + {name: "no-agent equals 0 clears", args: []string{"--no-agent=0"}, want: 0}, + {name: "agent then agent false clears", args: []string{"--agent", "--agent=false"}, want: 0}, + {name: "no-agent then no-agent false clears", args: []string{"--no-agent", "--no-agent=false"}, want: 0}, + + // pflag uses strconv.ParseBool, which rejects yes/on/etc. (parseBoolish + // accepts them, but that's only for the DCI_AGENT_MODE env var). + {name: "agent yes rejected (not pflag bool)", args: []string{"--agent=yes"}, want: 0}, + {name: "agent unrecognized value ignored", args: []string{"--agent=maybe"}, want: 0}, + + // Conflicting flags both true: most recently enabled wins. + {name: "conflict no-agent last wins", args: []string{"--agent", "--no-agent"}, want: -1}, + {name: "conflict agent last wins", args: []string{"--no-agent", "--agent"}, want: 1}, + {name: "stop at terminator", args: []string{"--", "--agent"}, want: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := agentFlagOverride(tt.args); got != tt.want { + t.Fatalf("agentFlagOverride(%v) = %d, want %d", tt.args, got, tt.want) + } + }) + } +} + +func TestBuildUserAgent(t *testing.T) { + // The mode token is always present and self-describing so analytics can + // segment by interface across all three classifications. + cases := []struct { + mode uaMode + want string + }{ + {uaModeInteractive, "mode=interactive"}, + {uaModeAgent, "mode=agent"}, + {uaModeNonInteractive, "mode=noninteractive"}, + } + for _, c := range cases { + ua := buildUserAgent(c.mode) + if !strings.HasPrefix(ua, "dci-cli/") { + t.Fatalf("unexpected User-Agent prefix: %q", ua) + } + if !strings.Contains(ua, c.want) { + t.Fatalf("buildUserAgent(%q) = %q, want it to contain %q", c.mode, ua, c.want) + } + // Guard against the legacy opaque tag regressing. + if strings.Contains(ua, "agent=1") { + t.Fatalf("User-Agent must use mode=, not the legacy agent=1 tag: %q", ua) + } + } + + // An unset mode falls back to interactive rather than emitting "mode=". + if ua := buildUserAgent(""); !strings.Contains(ua, "mode=interactive") { + t.Fatalf("empty mode should default to interactive: %q", ua) + } +} + +func TestDefaultOutputFormat(t *testing.T) { + t.Cleanup(func() { agentMode = false }) + + agentMode = false + if got := defaultOutputFormat(); got != "table" { + t.Fatalf("human defaultOutputFormat() = %q, want table", got) + } + agentMode = true + if got := defaultOutputFormat(); got != "toon" { + t.Fatalf("agent defaultOutputFormat() = %q, want toon", got) + } +} + +func TestDetectedAgentEnv(t *testing.T) { + for _, name := range agentEnvVars { + t.Setenv(name, "") + } + if got := detectedAgentEnv(); got != "" { + t.Fatalf("detectedAgentEnv() with no agent vars = %q, want empty", got) + } + + t.Setenv("KIRO_AGENT", "1") + if got := detectedAgentEnv(); got != "KIRO_AGENT" { + t.Fatalf("detectedAgentEnv() = %q, want KIRO_AGENT", got) + } +} + func TestCLIIntegrationBehavior(t *testing.T) { bin := buildBinary(t) diff --git a/skills/dci-cli/SKILL.md b/skills/dci-cli/SKILL.md index 512c957..68578d8 100644 --- a/skills/dci-cli/SKILL.md +++ b/skills/dci-cli/SKILL.md @@ -7,7 +7,9 @@ description: Operate the DoiT Cloud Intelligence CLI (`dci`) for DoiT Cloud Inte ## Overview -Use `dci` as the primary interface for DoiT Cloud Intelligence CLI tasks. Prefer read-only discovery first, prefer `--output json` for agent work, and use env-scoped `DCI_CUSTOMER_CONTEXT=` when switching customer context temporarily. +Use `dci` as the primary interface for DoiT Cloud Intelligence CLI tasks. Prefer read-only discovery first, prefer `--output toon` (compact and token-efficient; `--output json` when you need standard JSON) for agent work, and use env-scoped `DCI_CUSTOMER_CONTEXT=` when switching customer context temporarily. + +Set `DCI_AGENT_MODE=1` (or pass `--agent`) to run in agent mode: output defaults to compact TOON, terminal decoration is disabled, and banners/hints are routed to stderr so stdout stays parseable. `dci` also auto-detects common agent environments, so this is usually already on — run `dci status` to confirm. ## Quick Start diff --git a/skills/dci-cli/references/capabilities.md b/skills/dci-cli/references/capabilities.md index 527e3e4..5babe94 100644 --- a/skills/dci-cli/references/capabilities.md +++ b/skills/dci-cli/references/capabilities.md @@ -4,8 +4,8 @@ Use this file when you need the command map, not the procedural guidance. ## Invocation Patterns -- Flags-only read commands: `dci list-alerts --output json` -- Positional-ID read commands: `dci get-alert --output json` +- Flags-only read commands: `dci list-alerts --output toon` +- Positional-ID read commands: `dci get-alert --output toon` - Inline shorthand bodies: `dci invite-user email: user@example.com, organizationId: , roleId: ` - Inline SQL shorthand with `query`: `dci query body.query:"SELECT * FROM LIMIT 10"` - Stdin JSON bodies: `dci query < query.json` @@ -52,7 +52,7 @@ dci ## Working Rules -- Prefer `--output json` for agent workflows. +- Prefer `--output toon` for agent workflows (compact, token-efficient; it is also the agent-mode default). Use `--output json` when standard JSON is required. - Run `dci --help` before drafting complex request bodies. - Prefer read-only commands before mutation. - Treat auth, permissions, and `customerContext` as separate concerns. diff --git a/skills/dci-cli/references/cost-optimization.md b/skills/dci-cli/references/cost-optimization.md index 87fe376..b96d178 100644 --- a/skills/dci-cli/references/cost-optimization.md +++ b/skills/dci-cli/references/cost-optimization.md @@ -21,7 +21,7 @@ Do not assume the local saved context should be overwritten. Use SQL shorthand when the user wants a quick billing-table pass: ```bash -dci query body.query:"SELECT service_description, SUM(cost) AS total_cost FROM GROUP BY 1 ORDER BY 2 DESC LIMIT 10" --output json +dci query body.query:"SELECT service_description, SUM(cost) AS total_cost FROM GROUP BY 1 ORDER BY 2 DESC LIMIT 10" --output toon ``` Use this to identify the largest service pools quickly. diff --git a/skills/dci-cli/references/evals.md b/skills/dci-cli/references/evals.md index 6397181..c216061 100644 --- a/skills/dci-cli/references/evals.md +++ b/skills/dci-cli/references/evals.md @@ -60,7 +60,7 @@ Expected behavior: - choose stdin JSON as the primary query mode - provide a valid JSON payload example for `dci query < query.json` -- prefer `--output json` +- prefer `--output toon` (or `--output json` for standard JSON) ## Eval 6: Temporary Customer Switch @@ -107,7 +107,7 @@ Expected behavior: - use `ask-ava-sync` (not streaming) - set `ephemeral: true` for a one-shot question -- use `--output json` +- use `--output toon` (or `--output json` for standard JSON) - do not attempt `ava-feedback` after a sync call (no `answerId` available) ## Pass Criteria diff --git a/skills/dci-cli/references/examples.md b/skills/dci-cli/references/examples.md index 730eddb..623c610 100644 --- a/skills/dci-cli/references/examples.md +++ b/skills/dci-cli/references/examples.md @@ -26,11 +26,11 @@ dci customer-context set ## Discovery and Read-Only Navigation ```bash -dci list-alerts --output json -dci get-alert --output json -dci list-dimensions --output json -dci list-users --output json -dci list-platforms --output json +dci list-alerts --output toon +dci get-alert --output toon +dci list-dimensions --output toon +dci list-users --output toon +dci list-platforms --output toon ``` ## Query Examples @@ -38,13 +38,13 @@ dci list-platforms --output json Quick SQL shorthand: ```bash -dci query body.query:"SELECT * FROM LIMIT 10" --output json +dci query body.query:"SELECT * FROM LIMIT 10" --output toon ``` Service aggregation with SQL shorthand: ```bash -dci query body.query:"SELECT service_description, SUM(cost) AS total_cost FROM GROUP BY 1 ORDER BY 2 DESC LIMIT 10" --output json +dci query body.query:"SELECT service_description, SUM(cost) AS total_cost FROM GROUP BY 1 ORDER BY 2 DESC LIMIT 10" --output toon ``` Structured JSON query: @@ -95,15 +95,15 @@ dci query < query.json ## Report Drill-Down ```bash -dci list-reports --output json -dci get-report --output json -dci get-report-config --output json +dci list-reports --output toon +dci get-report --output toon +dci get-report-config --output toon ``` Override the time range when supported: ```bash -dci get-report --time-range P30D --output json +dci get-report --time-range P30D --output toon ``` ## Safe Mutation Templates @@ -124,7 +124,7 @@ Agents should prefer `ask-ava-sync` over `ask-ava-streaming`. The sync endpoint One-shot question (recommended for agents): ```bash -dci ask-ava-sync ephemeral: true, question: "What are my top 3 cost drivers this month?" --output json +dci ask-ava-sync ephemeral: true, question: "What are my top 3 cost drivers this month?" --output toon ``` Response shape: @@ -138,10 +138,10 @@ Response shape: Multi-turn conversation (set `ephemeral: false` to get a `conversationId`): ```bash -dci ask-ava-sync ephemeral: false, question: "What are my top cost drivers?" --output json +dci ask-ava-sync ephemeral: false, question: "What are my top cost drivers?" --output toon # response includes "conversationId": "" -dci ask-ava-sync ephemeral: false, conversationId: , question: "Break down EC2 by region" --output json +dci ask-ava-sync ephemeral: false, conversationId: , question: "Break down EC2 by region" --output toon ``` Delete a conversation when done: diff --git a/skills/dci-cli/references/query-patterns.md b/skills/dci-cli/references/query-patterns.md index 8695338..fae1f67 100644 --- a/skills/dci-cli/references/query-patterns.md +++ b/skills/dci-cli/references/query-patterns.md @@ -21,13 +21,13 @@ Use stdin JSON when: Quick inspection: ```bash -dci query body.query:"SELECT * FROM LIMIT 10" --output json +dci query body.query:"SELECT * FROM LIMIT 10" --output toon ``` Top services by spend: ```bash -dci query body.query:"SELECT service_description, SUM(cost) AS total_cost FROM GROUP BY 1 ORDER BY 2 DESC LIMIT 10" --output json +dci query body.query:"SELECT service_description, SUM(cost) AS total_cost FROM GROUP BY 1 ORDER BY 2 DESC LIMIT 10" --output toon ``` Practical notes: @@ -87,5 +87,5 @@ dci query < query.json - Offer SQL shorthand first only when the user asks for SQL or wants a very quick billing-table check. - Offer JSON first for reusable cost reports, dashboards, or 30-day optimization workflows. -- Prefer `--output json`. +- Prefer `--output toon` (compact, token-efficient; the agent-mode default). Use `--output json` when you need standard JSON, e.g. to pipe into `jq`. - Use env-scoped `DCI_CUSTOMER_CONTEXT=` if a customer switch is needed temporarily.