in main.go [156:323]
func run(ctx context.Context) error {
// Command line flags
var opt Options
opt.InitDefaults()
if err := opt.LoadConfigurationFile(); err != nil {
return fmt.Errorf("loading configuration file: %w", err)
}
maxIterations := flag.Int("max-iterations", 20, "maximum number of iterations agent will try before giving up")
kubeconfig := flag.String("kubeconfig", "", "path to the kubeconfig file")
promptTemplateFile := flag.String("prompt-template-file", "", "path to custom prompt template file")
tracePath := flag.String("trace-path", "trace.log", "path to the trace file")
removeWorkDir := flag.Bool("remove-workdir", false, "remove the temporary working directory after execution")
flag.StringVar(&opt.ProviderID, "llm-provider", opt.ProviderID, "language model provider")
flag.StringVar(&opt.ModelID, "model", opt.ModelID, "language model e.g. gemini-2.0-flash-thinking-exp-01-21, gemini-2.0-flash")
flag.BoolVar(&opt.SkipPermissions, "skip-permissions", opt.SkipPermissions, "(dangerous) skip asking for confirmation before executing kubectl commands that modify resources")
flag.BoolVar(&opt.MCPServer, "mcp-server", opt.MCPServer, "run in MCP server mode")
flag.BoolVar(&opt.EnableToolUseShim, "enable-tool-use-shim", opt.EnableToolUseShim, "enable tool use shim")
flag.BoolVar(&opt.Quiet, "quiet", opt.Quiet, "run in non-interactive mode, requires a query to be provided as a positional argument")
// add commandline flags for logging
klog.InitFlags(nil)
flag.Set("logtostderr", "false") // disable logging to stderr
flag.Set("log_file", filepath.Join(os.TempDir(), "kubectl-ai.log"))
flag.Parse()
defer klog.Flush()
// Do this early, before the third-party code logs anything.
redirectStdLogToKlog()
// Handle kubeconfig with priority: command-line arg > env var > default path
kubeconfigPath := *kubeconfig
if kubeconfigPath == "" {
// Check environment variable
kubeconfigPath = os.Getenv("KUBECONFIG")
if kubeconfigPath == "" {
// Use default path
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("error getting user home directory: %w", err)
}
kubeconfigPath = filepath.Join(homeDir, ".kube", "config")
}
}
if opt.MCPServer {
workDir := filepath.Join(os.TempDir(), "kubectl-ai-mcp")
if err := os.MkdirAll(workDir, 0755); err != nil {
return fmt.Errorf("error creating work directory: %w", err)
}
mcpServer, err := newKubectlMCPServer(ctx, kubeconfigPath, tools.Default(), workDir)
if err != nil {
return fmt.Errorf("creating mcp server: %w", err)
}
return mcpServer.Serve(ctx)
}
// Check for positional arguments (after all flags are parsed)
args := flag.Args()
var queryFromCmd string
// Check if stdin has data (is not a terminal)
stdinInfo, _ := os.Stdin.Stat()
stdinHasData := (stdinInfo.Mode() & os.ModeCharDevice) == 0
// Handle positional arguments and stdin
if len(args) > 1 {
return fmt.Errorf("only one positional argument (query) is allowed")
} else if stdinHasData {
// Read from stdin
scanner := bufio.NewScanner(os.Stdin)
var queryBuilder strings.Builder
// If we have a positional argument, use it as a prefix
if len(args) == 1 {
queryBuilder.WriteString(args[0])
queryBuilder.WriteString("\n")
}
// Read the rest from stdin
for scanner.Scan() {
queryBuilder.WriteString(scanner.Text())
queryBuilder.WriteString("\n")
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading from stdin: %w", err)
}
queryFromCmd = strings.TrimSpace(queryBuilder.String())
if queryFromCmd == "" {
return fmt.Errorf("no query provided from stdin")
}
} else if len(args) == 1 {
// Just use the positional argument as the query
queryFromCmd = args[0]
}
klog.Info("Application started", "pid", os.Getpid())
llmClient, err := gollm.NewClient(ctx, opt.ProviderID)
if err != nil {
return fmt.Errorf("creating llm client: %w", err)
}
defer llmClient.Close()
var recorder journal.Recorder
if *tracePath != "" {
fileRecorder, err := journal.NewFileRecorder(*tracePath)
if err != nil {
return fmt.Errorf("creating trace recorder: %w", err)
}
defer fileRecorder.Close()
recorder = fileRecorder
} else {
// Ensure we always have a recorder, to avoid nil checks
recorder = &journal.LogRecorder{}
defer recorder.Close()
}
doc := ui.NewDocument()
// since stdin is already consumed, we use TTY for taking input from user
useTTYForInput := stdinHasData
u, err := ui.NewTerminalUI(doc, recorder, useTTYForInput)
if err != nil {
return err
}
conversation := &agent.Conversation{
Model: opt.ModelID,
Kubeconfig: kubeconfigPath,
LLM: llmClient,
MaxIterations: *maxIterations,
PromptTemplateFile: *promptTemplateFile,
Tools: tools.Default(),
Recorder: recorder,
RemoveWorkDir: *removeWorkDir,
SkipPermissions: opt.SkipPermissions,
EnableToolUseShim: opt.EnableToolUseShim,
}
err = conversation.Init(ctx, doc)
if err != nil {
return fmt.Errorf("starting conversation: %w", err)
}
defer conversation.Close()
chatSession := session{
model: opt.ModelID,
doc: doc,
ui: u,
conversation: conversation,
LLM: llmClient,
}
if opt.Quiet {
if queryFromCmd == "" {
return fmt.Errorf("quiet mode requires a query to be provided as a positional argument")
}
return chatSession.answerQuery(ctx, queryFromCmd)
}
return chatSession.repl(ctx, queryFromCmd)
}