11 tea "github.com/charmbracelet/bubbletea/v2"
12 "github.com/charmbracelet/crush/internal/app"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/db"
15 "github.com/charmbracelet/crush/internal/format"
16 "github.com/charmbracelet/crush/internal/llm/agent"
17 "github.com/charmbracelet/crush/internal/log"
18 "github.com/charmbracelet/crush/internal/tui"
19 "github.com/charmbracelet/crush/internal/version"
20 "github.com/charmbracelet/fang"
21 "github.com/charmbracelet/x/term"
22 "github.com/spf13/cobra"
25var rootCmd = &cobra.Command{
27 Short: "Terminal-based AI assistant for software development",
28 Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
29It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
30to assist developers in writing, debugging, and understanding code directly from the terminal.`,
32 # Run in interactive mode
35 # Run with debug logging
38 # Run with debug slog.in a specific directory
39 crush -d -c /path/to/project
44 # Run a single non-interactive prompt
45 crush -p "Explain the use of context in Go"
47 # Run a single non-interactive prompt with JSON output format
48 crush -p "Explain the use of context in Go" -f json
50 RunE: func(cmd *cobra.Command, args []string) error {
52 debug, _ := cmd.Flags().GetBool("debug")
53 cwd, _ := cmd.Flags().GetString("cwd")
54 prompt, _ := cmd.Flags().GetString("prompt")
55 outputFormat, _ := cmd.Flags().GetString("output-format")
56 quiet, _ := cmd.Flags().GetBool("quiet")
58 // Validate format option
59 if !format.IsValid(outputFormat) {
60 return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText())
66 return fmt.Errorf("failed to change directory: %v", err)
72 return fmt.Errorf("failed to get current working directory: %v", err)
77 cfg, err := config.Init(cwd, debug)
82 // Create main context for the application
83 ctx, cancel := context.WithCancel(context.Background())
86 // Connect DB, this will also run migrations
87 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
92 app, err := app.New(ctx, conn, cfg)
94 slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
97 // Defer shutdown here so it runs for both interactive and non-interactive modes
100 // Initialize MCP tools early for both modes
101 initMCPTools(ctx, app, cfg)
103 prompt, err = maybePrependStdin(prompt)
105 slog.Error(fmt.Sprintf("Failed to read from stdin: %v", err))
109 // Non-interactive mode
111 // Run non-interactive flow using the App method
112 return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
116 program := tea.NewProgram(
119 tea.WithKeyReleases(),
120 tea.WithUniformKeyLayout(),
121 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
122 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
125 go app.Subscribe(program)
127 if _, err := program.Run(); err != nil {
128 slog.Error(fmt.Sprintf("TUI run error: %v", err))
129 return fmt.Errorf("TUI error: %v", err)
136func initMCPTools(ctx context.Context, app *app.App, cfg *config.Config) {
138 defer log.RecoverPanic("MCP-goroutine", nil)
140 // Create a context with timeout for the initial MCP tools fetch
141 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
144 // Set this up once with proper error handling
145 agent.GetMcpTools(ctxWithTimeout, app.Permissions, cfg)
146 slog.Info("MCP message handling goroutine exiting")
151 if err := fang.Execute(
152 context.Background(),
154 fang.WithVersion(version.Version),
161 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
163 rootCmd.Flags().BoolP("help", "h", false, "Help")
164 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
165 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
167 // Add format flag with validation logic
168 rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
169 "Output format for non-interactive mode (text, json)")
171 // Add quiet flag to hide spinner in non-interactive mode
172 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
174 // Register custom validation for the format flag
175 rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
176 return format.SupportedFormats, cobra.ShellCompDirectiveNoFileComp
180func maybePrependStdin(prompt string) (string, error) {
181 if term.IsTerminal(os.Stdin.Fd()) {
184 fi, err := os.Stdin.Stat()
188 if fi.Mode()&os.ModeNamedPipe == 0 {
191 bts, err := io.ReadAll(os.Stdin)
195 return string(bts) + "\n\n" + prompt, nil
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list