package app import ( "context" "errors" "flag" "fmt" "net/http" "os" "os/signal" "path/filepath" "strings" "agentu/internal/agent" "agentu/internal/session" "agentu/internal/tools" "agentu/internal/tui" "agentu/pkg/config" "agentu/pkg/llm" ) type cliOptions struct { configPath string yolo bool plain bool themeName string resumeID string } func Run() error { opts := parseCLIOptions() themeMode, err := tui.ParseThemeMode(opts.themeName) if err != nil { return err } cfg, err := loadConfig(opts.configPath) if err != nil { return err } providerName, providerConfig, provider := defaultProvider(cfg, http.DefaultClient) assistant := newAssistant(cfg, providerName, providerConfig, provider, opts.yolo, http.DefaultClient) modelManager, err := newSessionManager(cfg, assistant, http.DefaultClient) if err != nil { return err } if err := initializeSession(modelManager, opts.resumeID); err != nil { return err } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() return runInterface(ctx, assistant, modelManager, providerConfig, cfg.Agent.WorkingDir, opts, themeMode) } func parseCLIOptions() cliOptions { configPath := flag.String("config", config.DefaultPath, "path to YAML config") yolo := flag.Bool("yolo", false, "also enable automatic file writes and shell execution") plain := flag.Bool("plain", false, "use the simple line-based REPL instead of the TUI") themeName := flag.String("theme", "light", "TUI theme: light or dark") resumeID := flag.String("resume", "", "resume a specific session by ID") flag.Parse() return cliOptions{ configPath: *configPath, yolo: *yolo, plain: *plain, themeName: *themeName, resumeID: *resumeID, } } func loadConfig(configPath string) (*config.Config, error) { cfg, err := config.Load(configPath) if err == nil { return cfg, nil } if !errors.Is(err, os.ErrNotExist) { return nil, err } if bootstrapErr := bootstrapConfig(configPath); bootstrapErr != nil { return nil, bootstrapErr } return nil, fmt.Errorf("created default config at %s; edit it to set your API key and provider, then restart agentu", configPath) } func defaultProvider(cfg *config.Config, httpClient *http.Client) (string, config.ProviderConfig, llm.Provider) { providerName := cfg.DefaultProviderName() providerConfig := cfg.Providers[providerName] provider := llm.NewOpenAICompatibleClient(providerConfig.BaseURL, providerConfig.APIKey, httpClient) return providerName, providerConfig, provider } func newAssistant(cfg *config.Config, providerName string, providerConfig config.ProviderConfig, provider llm.Provider, yolo bool, httpClient *http.Client) *agent.Agent { return agent.New(agent.Options{ Provider: provider, ProviderName: providerName, Model: providerConfig.Model, Thinking: providerConfig.Thinking, ThinkingParam: providerConfig.ThinkingParam, ThinkingEnabled: providerConfig.ThinkingConfigured, SystemPrompt: systemPrompt(cfg, yolo), ToolRegistry: newToolRegistry(cfg.Agent.WorkingDir, yolo, httpClient), ToolTimeout: cfg.Agent.ToolTimeout, MaxContextTokens: maxContextTokens(cfg, providerConfig), }) } func systemPrompt(cfg *config.Config, yolo bool) string { instructions := tools.AgentInstructions(cfg.Agent.WorkingDir, yolo) return strings.TrimSpace(cfg.Agent.SystemPrompt + "\n\n" + instructions) } func maxContextTokens(cfg *config.Config, providerConfig config.ProviderConfig) int { if cfg.Agent.MaxContextTokens != 0 { return cfg.Agent.MaxContextTokens } return providerConfig.ContextTokens } func newToolRegistry(workingDir string, yolo bool, httpClient *http.Client) *tools.Registry { var registry *tools.Registry if yolo { registry = tools.Builtins(workingDir) } else { registry = tools.ReadOnlyBuiltins(workingDir) } registry.Register(tools.NewBuiltinSearchTool(httpClient)) registry.Register(tools.NewBuiltinFetchTool(httpClient)) return registry } func newSessionManager(cfg *config.Config, assistant *agent.Agent, httpClient *http.Client) (*session.Manager, error) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("resolve home dir: %w", err) } sessionsDir := filepath.Join(home, ".agentu", "sessions") store, err := session.NewStore(sessionsDir) if err != nil { return nil, fmt.Errorf("init session store: %w", err) } return session.NewManager(cfg, assistant, httpClient, store) } func runInterface(ctx context.Context, assistant *agent.Agent, modelManager *session.Manager, providerConfig config.ProviderConfig, workingDir string, opts cliOptions, themeMode tui.ThemeMode) error { if opts.plain { return repl(ctx, assistant, modelManager) } err := tui.Run(ctx, assistant, tui.Options{ ModelName: providerConfig.Model, Yolo: opts.yolo, ThemeMode: themeMode, ModelManager: modelManager, WorkingDir: workingDir, }) printExitSession(modelManager) return err }