Claude Code Plugins

Community-maintained marketplace

Feedback

Build CLI tools using Go with Cobra and Viper. Use for implementing agentctl commands, interactive prompts, configuration management, and output formatting. Triggers on "CLI", "agentctl", "command line", "cobra", "terminal application", "interactive prompt", or when implementing spec/009-developer-experience.md CLI section.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name agentctl-cli
description Build CLI tools using Go with Cobra and Viper. Use for implementing agentctl commands, interactive prompts, configuration management, and output formatting. Triggers on "CLI", "agentctl", "command line", "cobra", "terminal application", "interactive prompt", or when implementing spec/009-developer-experience.md CLI section.

agentctl CLI Development

Overview

Build the agentctl CLI tool for AgentStack platform interaction. Implements authentication, project management, agent operations, development workflows, and evaluation commands.

CLI Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     agentctl Architecture                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    cmd/agentctl/                          │  │
│  │  main.go → root.go → [auth|project|agent|dev|eval].go   │  │
│  └──────────────────────────────────────────────────────────┘  │
│                           │                                     │
│                           ▼                                     │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                  internal/cli/                            │  │
│  │  config/ │ client/ │ output/ │ prompt/ │ spinner/        │  │
│  └──────────────────────────────────────────────────────────┘  │
│                           │                                     │
│                           ▼                                     │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                 pkg/agentstack/                           │  │
│  │  SDK client for API communication                         │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Project Structure

agentctl/
├── cmd/agentctl/
│   ├── main.go
│   ├── root.go           # Root command, global flags
│   ├── auth.go           # auth login, logout, whoami
│   ├── project.go        # project create, list, switch
│   ├── agent.go          # agent init, deploy, list, logs, delete
│   ├── dev.go            # dev (local development)
│   ├── eval.go           # eval run, dataset, report, compare
│   └── version.go        # version command
├── internal/cli/
│   ├── config/           # Configuration management
│   ├── client/           # API client wrapper
│   ├── output/           # Table, JSON, YAML output
│   ├── prompt/           # Interactive prompts
│   └── spinner/          # Progress indicators
├── pkg/agentstack/       # SDK (can be external package)
├── templates/            # Agent scaffolding templates
└── Makefile

Root Command Setup

// cmd/agentctl/root.go
package main

import (
    "os"
    
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var (
    cfgFile string
    output  string
)

var rootCmd = &cobra.Command{
    Use:   "agentctl",
    Short: "AgentStack CLI - Deploy and manage AI agents",
    Long: `agentctl is the command-line interface for the AgentStack platform.
    
It allows you to create, deploy, and manage AI agents with full 
observability and evaluation capabilities.`,
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        return initConfig()
    },
}

func init() {
    // Global flags
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", 
        "config file (default is $HOME/.agentctl/config.yaml)")
    rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "table", 
        "output format: table, json, yaml")
    rootCmd.PersistentFlags().Bool("no-color", false, 
        "disable colored output")
    
    // Bind to viper
    viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output"))
    
    // Add subcommands
    rootCmd.AddCommand(authCmd)
    rootCmd.AddCommand(projectCmd)
    rootCmd.AddCommand(agentCmd)
    rootCmd.AddCommand(devCmd)
    rootCmd.AddCommand(evalCmd)
    rootCmd.AddCommand(versionCmd)
}

func initConfig() error {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            return err
        }
        viper.AddConfigPath(home + "/.agentctl")
        viper.SetConfigName("config")
        viper.SetConfigType("yaml")
    }
    
    viper.SetEnvPrefix("AGENTCTL")
    viper.AutomaticEnv()
    
    viper.ReadInConfig() // Ignore error if not exists
    return nil
}

func main() {
    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

Authentication Commands

// cmd/agentctl/auth.go
package main

import (
    "fmt"
    
    "github.com/spf13/cobra"
)

var authCmd = &cobra.Command{
    Use:   "auth",
    Short: "Manage authentication",
}

var authLoginCmd = &cobra.Command{
    Use:   "login",
    Short: "Login to AgentStack",
    RunE: func(cmd *cobra.Command, args []string) error {
        token, _ := cmd.Flags().GetString("token")
        
        if token != "" {
            return loginWithToken(token)
        }
        return loginBrowser()
    },
}

var authWhoamiCmd = &cobra.Command{
    Use:   "whoami",
    Short: "Display current identity",
    RunE: func(cmd *cobra.Command, args []string) error {
        cfg := config.Load()
        if cfg.Token == "" {
            return fmt.Errorf("not logged in, run: agentctl auth login")
        }
        
        client := client.New(cfg)
        user, err := client.Auth.WhoAmI(cmd.Context())
        if err != nil {
            return err
        }
        
        output.Print(user, output.Format(viper.GetString("output")))
        return nil
    },
}

func init() {
    authLoginCmd.Flags().String("token", "", "API token for non-interactive login")
    
    authCmd.AddCommand(authLoginCmd)
    authCmd.AddCommand(authWhoamiCmd)
    authCmd.AddCommand(&cobra.Command{
        Use:   "logout",
        Short: "Logout from AgentStack",
        RunE: func(cmd *cobra.Command, args []string) error {
            return config.ClearCredentials()
        },
    })
}

func loginBrowser() error {
    // Open browser for OAuth flow
    fmt.Println("Opening browser for login...")
    
    // Start local server for callback
    server := oauth.NewCallbackServer(8765)
    go server.Start()
    
    // Open browser
    url := fmt.Sprintf("%s/auth/cli?port=8765", config.Load().Endpoint)
    browser.Open(url)
    
    // Wait for token
    token := <-server.TokenChan
    
    // Save token
    return config.SaveCredentials(token)
}

Agent Commands

// cmd/agentctl/agent.go
package main

import (
    "fmt"
    "os"
    "text/template"
    
    "github.com/spf13/cobra"
)

var agentCmd = &cobra.Command{
    Use:   "agent",
    Short: "Manage agents",
}

var agentInitCmd = &cobra.Command{
    Use:   "init <name>",
    Short: "Scaffold a new agent",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        name := args[0]
        tmpl, _ := cmd.Flags().GetString("template")
        
        return scaffoldAgent(name, tmpl)
    },
}

var agentDeployCmd = &cobra.Command{
    Use:   "deploy [path]",
    Short: "Deploy an agent to the platform",
    Args:  cobra.MaximumNArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        path := "."
        if len(args) > 0 {
            path = args[0]
        }
        
        wait, _ := cmd.Flags().GetBool("wait")
        return deployAgent(cmd.Context(), path, wait)
    },
}

var agentLogsCmd = &cobra.Command{
    Use:   "logs <name>",
    Short: "Stream agent logs",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        follow, _ := cmd.Flags().GetBool("follow")
        tail, _ := cmd.Flags().GetInt("tail")
        
        return streamLogs(cmd.Context(), args[0], follow, tail)
    },
}

func init() {
    agentInitCmd.Flags().StringP("template", "t", "google-adk", 
        "agent template: google-adk, langchain, custom")
    
    agentDeployCmd.Flags().BoolP("wait", "w", false, 
        "wait for deployment to complete")
    
    agentLogsCmd.Flags().BoolP("follow", "f", false, "follow log output")
    agentLogsCmd.Flags().IntP("tail", "n", 100, "number of lines to show")
    
    agentCmd.AddCommand(agentInitCmd)
    agentCmd.AddCommand(agentDeployCmd)
    agentCmd.AddCommand(agentLogsCmd)
    agentCmd.AddCommand(&cobra.Command{
        Use:   "list",
        Short: "List agents",
        RunE:  listAgents,
    })
    agentCmd.AddCommand(&cobra.Command{
        Use:   "delete <name>",
        Short: "Delete an agent",
        Args:  cobra.ExactArgs(1),
        RunE:  deleteAgent,
    })
}

func scaffoldAgent(name, tmpl string) error {
    spinner := spinner.New("Creating agent: " + name)
    spinner.Start()
    
    // Create directory
    if err := os.MkdirAll(name, 0755); err != nil {
        spinner.Fail("Failed to create directory")
        return err
    }
    spinner.Success("Created directory structure")
    
    // Generate files from template
    files := templates.Get(tmpl)
    for _, f := range files {
        spinner.Update("Generating " + f.Name)
        if err := generateFile(name, f); err != nil {
            spinner.Fail("Failed to generate " + f.Name)
            return err
        }
        spinner.Success("Generated " + f.Name)
    }
    
    // Print next steps
    fmt.Printf("\n✓ Agent scaffolded: %s\n\n", name)
    fmt.Println("Next steps:")
    fmt.Printf("  cd %s\n", name)
    fmt.Println("  agentctl dev          # Start local development")
    fmt.Println("  agentctl deploy       # Deploy to platform")
    
    return nil
}

Development Commands

// cmd/agentctl/dev.go
package main

import (
    "context"
    "fmt"
    "os"
    "os/exec"
    "os/signal"
    
    "github.com/spf13/cobra"
)

var devCmd = &cobra.Command{
    Use:   "dev",
    Short: "Start local development server",
    RunE:  runDev,
}

func init() {
    devCmd.Flags().IntP("port", "p", 8080, "port to run on")
    devCmd.Flags().Bool("mock-llm", false, "use mock LLM provider")
}

func runDev(cmd *cobra.Command, args []string) error {
    port, _ := cmd.Flags().GetInt("port")
    mockLLM, _ := cmd.Flags().GetBool("mock-llm")
    
    fmt.Println("Starting local development server...")
    
    // Check for agent.yaml
    if _, err := os.Stat("agent.yaml"); os.IsNotExist(err) {
        return fmt.Errorf("agent.yaml not found. Run this from an agent directory")
    }
    
    spinner := spinner.New("Loading agent.yaml")
    spinner.Start()
    
    agentCfg, err := loadAgentConfig("agent.yaml")
    if err != nil {
        spinner.Fail("Failed to load agent.yaml")
        return err
    }
    spinner.Success("Loaded agent.yaml")
    
    // Build container
    spinner.Update("Building container")
    if err := buildContainer(); err != nil {
        spinner.Fail("Build failed")
        return err
    }
    spinner.Success("Built container")
    
    // Start dependencies
    spinner.Update("Starting dependencies")
    deps, err := startDependencies(mockLLM)
    if err != nil {
        spinner.Fail("Failed to start dependencies")
        return err
    }
    defer deps.Stop()
    spinner.Success("Started dependencies")
    
    // Start agent
    spinner.Update("Starting agent")
    agent, err := startAgent(port, agentCfg)
    if err != nil {
        spinner.Fail("Failed to start agent")
        return err
    }
    spinner.Success(fmt.Sprintf("Agent running at http://localhost:%d", port))
    
    // Print endpoints
    fmt.Println("\nEndpoints:")
    fmt.Printf("  Chat:    POST http://localhost:%d/chat\n", port)
    fmt.Printf("  SSE:     POST http://localhost:%d/chat/stream\n", port)
    fmt.Printf("  Health:  GET  http://localhost:%d/health\n", port)
    fmt.Println("\nWatching for changes... (Ctrl+C to stop)")
    
    // Watch for file changes
    go watchFiles(agent)
    
    // Wait for interrupt
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()
    <-ctx.Done()
    
    fmt.Println("\nShutting down...")
    agent.Stop()
    
    return nil
}

Evaluation Commands

// cmd/agentctl/eval.go
package main

import (
    "fmt"
    
    "github.com/spf13/cobra"
)

var evalCmd = &cobra.Command{
    Use:   "eval",
    Short: "Run agent evaluations",
}

var evalRunCmd = &cobra.Command{
    Use:   "run [agent]",
    Short: "Run evaluation suite",
    Args:  cobra.MaximumNArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        agent := ""
        if len(args) > 0 {
            agent = args[0]
        }
        
        dataset, _ := cmd.Flags().GetString("dataset")
        scorers, _ := cmd.Flags().GetStringSlice("scorer")
        
        return runEvaluation(cmd.Context(), agent, dataset, scorers)
    },
}

var evalCompareCmd = &cobra.Command{
    Use:   "compare <run1> <run2>",
    Short: "Compare two evaluation runs",
    Args:  cobra.ExactArgs(2),
    RunE: func(cmd *cobra.Command, args []string) error {
        return compareRuns(cmd.Context(), args[0], args[1])
    },
}

func init() {
    evalRunCmd.Flags().StringP("dataset", "d", "", "evaluation dataset")
    evalRunCmd.Flags().StringSliceP("scorer", "s", nil, 
        "scorers to use (can specify multiple)")
    
    evalCmd.AddCommand(evalRunCmd)
    evalCmd.AddCommand(evalCompareCmd)
    evalCmd.AddCommand(&cobra.Command{
        Use:   "report <run-id>",
        Short: "View evaluation report",
        Args:  cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            return showReport(cmd.Context(), args[0])
        },
    })
}

func runEvaluation(ctx context.Context, agent, dataset string, scorers []string) error {
    spinner := spinner.New("Running evaluation")
    spinner.Start()
    
    client := client.New(config.Load())
    
    // Start evaluation
    run, err := client.Eval.Start(ctx, &EvalRequest{
        AgentID:  agent,
        Dataset:  dataset,
        Scorers:  scorers,
    })
    if err != nil {
        spinner.Fail("Failed to start evaluation")
        return err
    }
    
    // Poll for completion
    for run.Status == "running" {
        spinner.Update(fmt.Sprintf("Running... %d/%d", run.Completed, run.Total))
        time.Sleep(2 * time.Second)
        run, _ = client.Eval.Get(ctx, run.ID)
    }
    
    if run.Status == "failed" {
        spinner.Fail("Evaluation failed")
        return fmt.Errorf("evaluation failed: %s", run.Error)
    }
    
    spinner.Success("Evaluation complete")
    
    // Print results
    fmt.Println("\nResults:")
    output.PrintTable(run.Results, []string{"Scorer", "Score", "Passed"})
    
    fmt.Printf("\nFull report: agentctl eval report %s\n", run.ID)
    
    return nil
}

Configuration Management

// internal/cli/config/config.go
package config

import (
    "os"
    "path/filepath"
    
    "gopkg.in/yaml.v3"
)

type Config struct {
    CurrentContext string     `yaml:"current-context"`
    Contexts       []Context  `yaml:"contexts"`
}

type Context struct {
    Name     string `yaml:"name"`
    Endpoint string `yaml:"endpoint"`
    Project  string `yaml:"project"`
    Token    string `yaml:"token,omitempty"`
}

func Load() *Config {
    home, _ := os.UserHomeDir()
    path := filepath.Join(home, ".agentctl", "config.yaml")
    
    data, err := os.ReadFile(path)
    if err != nil {
        return &Config{
            CurrentContext: "default",
            Contexts: []Context{{
                Name:     "default",
                Endpoint: "https://api.agentstack.io",
            }},
        }
    }
    
    var cfg Config
    yaml.Unmarshal(data, &cfg)
    return &cfg
}

func (c *Config) Current() *Context {
    for _, ctx := range c.Contexts {
        if ctx.Name == c.CurrentContext {
            return &ctx
        }
    }
    return nil
}

func (c *Config) Save() error {
    home, _ := os.UserHomeDir()
    dir := filepath.Join(home, ".agentctl")
    os.MkdirAll(dir, 0700)
    
    path := filepath.Join(dir, "config.yaml")
    data, _ := yaml.Marshal(c)
    return os.WriteFile(path, data, 0600)
}

Output Formatting

// internal/cli/output/output.go
package output

import (
    "encoding/json"
    "fmt"
    "os"
    
    "github.com/olekukonko/tablewriter"
    "gopkg.in/yaml.v3"
)

type Format string

const (
    FormatTable Format = "table"
    FormatJSON  Format = "json"
    FormatYAML  Format = "yaml"
)

func Print(v interface{}, format Format) {
    switch format {
    case FormatJSON:
        enc := json.NewEncoder(os.Stdout)
        enc.SetIndent("", "  ")
        enc.Encode(v)
    case FormatYAML:
        enc := yaml.NewEncoder(os.Stdout)
        enc.Encode(v)
    default:
        // Handle table format based on type
        printTable(v)
    }
}

func PrintTable(data [][]string, headers []string) {
    table := tablewriter.NewWriter(os.Stdout)
    table.SetHeader(headers)
    table.SetBorder(false)
    table.SetHeaderLine(true)
    table.AppendBulk(data)
    table.Render()
}

func Success(msg string) {
    fmt.Printf("✓ %s\n", msg)
}

func Error(msg string) {
    fmt.Fprintf(os.Stderr, "✗ %s\n", msg)
}

func Warning(msg string) {
    fmt.Printf("⚠ %s\n", msg)
}

Interactive Prompts

// internal/cli/prompt/prompt.go
package prompt

import (
    "github.com/AlecAivazis/survey/v2"
)

func Confirm(message string) (bool, error) {
    var result bool
    err := survey.AskOne(&survey.Confirm{
        Message: message,
    }, &result)
    return result, err
}

func Select(message string, options []string) (string, error) {
    var result string
    err := survey.AskOne(&survey.Select{
        Message: message,
        Options: options,
    }, &result)
    return result, err
}

func Input(message string, defaultValue string) (string, error) {
    var result string
    err := survey.AskOne(&survey.Input{
        Message: message,
        Default: defaultValue,
    }, &result)
    return result, err
}

func Password(message string) (string, error) {
    var result string
    err := survey.AskOne(&survey.Password{
        Message: message,
    }, &result)
    return result, err
}

Dependencies

github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.0
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/olekukonko/tablewriter v0.0.5
github.com/briandowns/spinner v1.23.0
github.com/fatih/color v1.16.0
github.com/fsnotify/fsnotify v1.7.0
gopkg.in/yaml.v3 v3.0.1

Testing

// cmd/agentctl/agent_test.go
package main

import (
    "bytes"
    "testing"
    
    "github.com/stretchr/testify/assert"
)

func TestAgentInit(t *testing.T) {
    // Create temp directory
    dir := t.TempDir()
    
    // Run init
    buf := new(bytes.Buffer)
    rootCmd.SetOut(buf)
    rootCmd.SetArgs([]string{"agent", "init", "test-agent", "-t", "google-adk"})
    
    err := rootCmd.Execute()
    assert.NoError(t, err)
    
    // Verify files created
    assert.FileExists(t, dir+"/test-agent/agent.yaml")
    assert.FileExists(t, dir+"/test-agent/Dockerfile")
    assert.DirExists(t, dir+"/test-agent/src")
}

Resources

  • references/cobra-patterns.md - Advanced Cobra patterns
  • assets/templates/ - Agent scaffolding templates