Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
Expand Down Expand Up @@ -37,3 +39,60 @@ func TestDocsCommandRejectsArguments(t *testing.T) {
t.Fatal("docs command accepted an argument")
}
}

func TestAgentOnboardingHintShowsOnce(t *testing.T) {
oldAgentMode := agentMode
oldEnvDetected := agentEnvDetected
agentMode = true
agentEnvDetected = "TEST_AGENT"
t.Cleanup(func() {
agentMode = oldAgentMode
agentEnvDetected = oldEnvDetected
})

configDir := t.TempDir()

captureStderr := func(fn func()) string {
old := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
fn()
if err := w.Close(); err != nil {
t.Fatalf("close stderr writer: %v", err)
}
os.Stderr = old
buf := make([]byte, 4096)
n, _ := r.Read(buf)
return string(buf[:n])
}

first := captureStderr(func() { maybeAgentOnboardingHint(configDir) })
if !strings.Contains(first, "dci skill") || !strings.Contains(first, "llms.txt") {
t.Fatalf("first hint = %q, want skill and llms.txt pointers", first)
}
if _, err := os.Stat(filepath.Join(configDir, "agent_onboarding_shown")); err != nil {
t.Fatalf("marker file not written: %v", err)
}

second := captureStderr(func() { maybeAgentOnboardingHint(configDir) })
if second != "" {
t.Fatalf("second run printed %q, want silence", second)
}
}

func TestAgentOnboardingHintSkippedInHumanMode(t *testing.T) {
oldAgentMode := agentMode
oldEnvDetected := agentEnvDetected
agentMode = false
agentEnvDetected = ""
t.Cleanup(func() {
agentMode = oldAgentMode
agentEnvDetected = oldEnvDetected
})

configDir := t.TempDir()
maybeAgentOnboardingHint(configDir)
if _, err := os.Stat(filepath.Join(configDir, "agent_onboarding_shown")); err == nil {
t.Fatal("marker written in human mode")
}
}
66 changes: 65 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ func run() (exitCode int) {
// Reset per-invocation state so repeated calls (e.g. in tests) start clean.
customerContextFlagValue = ""
resolvedCustomerContext = ""
helpFullRequested = false
requestReportCurrency = ""
bufferedRequestBody = nil
nonJSONErrorResponse = false
Expand Down Expand Up @@ -307,6 +308,7 @@ func run() (exitCode int) {
registerUpgradeCommand(configDir)
registerVersionCommand()
registerDocsCommand()
registerOpenCommand(configDir)
registerSkillCommands()
registerCommandCatalog()
if cachedTokenIsDoer() {
Expand All @@ -323,6 +325,7 @@ func run() (exitCode int) {
applyCustomerContext(configDir)
lockToDCI()
setupCompletion()
os.Args = rewriteHelpFullFlag(os.Args)
os.Args = normalizeArgs(os.Args)
if err := preflightAPIInvocation(os.Args); err != nil {
return reportExecutionError(err, 0, configDir)
Expand All @@ -341,6 +344,10 @@ func run() (exitCode int) {
code = exitServer
}
maybeHintDoerContext(code, cli.GetLastStatus(), configDir)
if code == 0 {
// Success only: failure stderr must stay a single parseable envelope.
maybeAgentOnboardingHint(configDir)
}
return code
}

Expand Down Expand Up @@ -419,6 +426,33 @@ func rejectProfileFlags(args []string) error {
return nil
}

var helpFullRequested bool

func rewriteHelpFullFlag(args []string) []string {
out := make([]string, 0, len(args))
for _, arg := range args {
if arg == "--help-full" {
helpFullRequested = true
out = append(out, "--help")
continue
}
out = append(out, arg)
}
return out
}

func terseHelpText(long string) (string, bool) {
idx := strings.Index(long, "## ")
if idx < 0 {
return long, false
}
head := strings.TrimSpace(long[:idx])
if head == "" {
head = "(no description)"
}
return head + "\n\nSchemas and examples: add --help-full", true
}

func normalizeArgs(args []string) []string {
if len(args) <= 1 {
return []string{args[0], "--help"}
Expand Down Expand Up @@ -792,6 +826,28 @@ func maybeHintAgentMode() {
fmt.Fprintln(os.Stderr, "Tip: set DCI_AGENT_MODE=1 (or pass --agent) for compact, parse-friendly output.")
}

// maybeAgentOnboardingHint prints a one-time stderr pointer when an agent
// environment is first seen with this config dir, so agents discover the
// embedded skill, the machine-readable catalog, and the docs without being
// told. Stderr only — stdout must stay parseable — and marker-gated so it
// never becomes per-command chatter.
func maybeAgentOnboardingHint(configDir string) {
if !agentMode || agentEnvDetected == "" {
return
}
marker := filepath.Join(configDir, "agent_onboarding_shown")
if _, err := os.Stat(marker); err == nil {
return
}
if err := os.WriteFile(marker, []byte(time.Now().UTC().Format(time.RFC3339)+"\n"), 0o600); err != nil {
return // cannot persist the marker; stay silent rather than repeat forever
}
fmt.Fprintln(os.Stderr, "Agent mode is active. Useful entry points:")
fmt.Fprintln(os.Stderr, " dci skill <agent> install CLI usage guidance for this agent (claude, codex, cursor, gemini, kiro, opencode)")
fmt.Fprintln(os.Stderr, " dci commands --json machine-readable command catalog (args, flags, destructive metadata)")
fmt.Fprintln(os.Stderr, " dci docs documentation entry points, incl. https://help.doit.com/llms.txt")
}

const dciUsageTemplate = `Usage:{{if .Runnable}}
{{.Use}}{{if .HasAvailableFlags}} [flags]{{end}}{{end}}{{if .HasAvailableSubCommands}}
dci [command]
Expand Down Expand Up @@ -822,7 +878,8 @@ Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
`

const dciLongDescription = "Command-line interface for the Cloud Intelligence™ API.\n\n" +
"Documentation: https://help.doit.com/docs/cli or run `dci docs` for every entry point."
"Documentation: https://help.doit.com/docs/cli or run `dci docs` for every entry point.\n" +
"AI agents: `dci skill <agent>` installs usage guidance; `dci commands --json` prints the machine-readable catalog."

var rootExamples = []string{
" dci status",
Expand Down Expand Up @@ -994,6 +1051,13 @@ func setupCompletion() {
defaultHelp := cli.Root.HelpFunc()
cli.Root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
sanitizeFlagPlaceholders(cmd)
if !helpFullRequested {
if terse, truncated := terseHelpText(cmd.Long); truncated {
originalLong := cmd.Long
cmd.Long = terse
defer func() { cmd.Long = originalLong }()
}
}
hasAPICommands := false
if cmd == cli.Root {
loadAPI()
Expand Down
29 changes: 29 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,34 @@ func TestBuildTableStringWithHiddenColumnsIncluded(t *testing.T) {
}
}

func TestTerseHelpText(t *testing.T) {
long := "Runs a report query.\n## Input Example\n```json\n{}\n```\n## Request Schema\n..."
terse, truncated := terseHelpText(long)
if !truncated {
t.Fatal("schema-bearing help not truncated")
}
if strings.Contains(terse, "## ") || !strings.Contains(terse, "--help-full") {
t.Errorf("terse = %q, want schemas stripped and --help-full pointer", terse)
}
if _, truncated := terseHelpText("plain description"); truncated {
t.Error("plain help truncated")
}
}

func TestRewriteHelpFullFlag(t *testing.T) {
oldRequested := helpFullRequested
helpFullRequested = false
t.Cleanup(func() { helpFullRequested = oldRequested })

args := rewriteHelpFullFlag([]string{"dci", "query", "--help-full"})
if !helpFullRequested {
t.Error("--help-full not detected")
}
if args[2] != "--help" {
t.Errorf("args = %v, want --help substituted", args)
}
}

func TestFormatValueNilIsEmptyCell(t *testing.T) {
if got := formatValue(nil); got != "" {
t.Errorf("formatValue(nil) = %q, want empty", got)
Expand Down Expand Up @@ -1945,6 +1973,7 @@ var expectedSkillFiles = []string{
"skills/dci-cli/references/cost-optimization.md",
"skills/dci-cli/references/evals.md",
"skills/dci-cli/references/examples.md",
"skills/dci-cli/references/finops-baseline.md",
"skills/dci-cli/references/query-patterns.md",
}

Expand Down
Loading
Loading