diff --git a/cmd/entire/cli/config.go b/cmd/entire/cli/config.go index d76e214034..078376a290 100644 --- a/cmd/entire/cli/config.go +++ b/cmd/entire/cli/config.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path/filepath" + "strings" "entire.io/cli/cmd/entire/cli/agent" "entire.io/cli/cmd/entire/cli/jsonutil" @@ -30,14 +31,6 @@ type EntireSettings struct { // Strategy is the name of the git strategy to use Strategy string `json:"strategy"` - // Agent is the name of the coding agent (e.g., "claude-code", "cursor") - // When empty, auto-detection is used - Agent string `json:"agent,omitempty"` - - // AgentAutoDetect controls whether to auto-detect the agent when not explicitly set - // Defaults to true for backwards compatibility - AgentAutoDetect *bool `json:"agent_auto_detect,omitempty"` - // Enabled indicates whether Entire is active. When false, CLI commands // show a disabled message and hooks exit silently. Defaults to true. Enabled bool `json:"enabled"` @@ -54,10 +47,6 @@ type EntireSettings struct { // StrategyOptions contains strategy-specific configuration StrategyOptions map[string]interface{} `json:"strategy_options,omitempty"` - // AgentOptions contains agent-specific configuration - // Keyed by agent name, e.g., {"claude-code": {"ignore_untracked": false}} - AgentOptions map[string]interface{} `json:"agent_options,omitempty"` - // Telemetry controls anonymous usage analytics. // nil = not asked yet (show prompt), true = opted in, false = opted out Telemetry *bool `json:"telemetry,omitempty"` @@ -166,41 +155,6 @@ func mergeSettingsJSON(settings *EntireSettings, data []byte) error { } } - // Override agent if present and non-empty - if agentRaw, ok := raw["agent"]; ok { - var a string - if err := json.Unmarshal(agentRaw, &a); err != nil { - return fmt.Errorf("parsing agent field: %w", err) - } - if a != "" { - settings.Agent = a - } - } - - // Override agent_auto_detect if present - if autoDetectRaw, ok := raw["agent_auto_detect"]; ok { - var ad bool - if err := json.Unmarshal(autoDetectRaw, &ad); err != nil { - return fmt.Errorf("parsing agent_auto_detect field: %w", err) - } - settings.AgentAutoDetect = &ad - } - - // Merge agent_options if present - if agentOptsRaw, ok := raw["agent_options"]; ok { - var opts map[string]interface{} - if err := json.Unmarshal(agentOptsRaw, &opts); err != nil { - return fmt.Errorf("parsing agent_options field: %w", err) - } - if settings.AgentOptions == nil { - settings.AgentOptions = opts - } else { - for k, v := range opts { - settings.AgentOptions[k] = v - } - } - } - // Override telemetry if present if telemetryRaw, ok := raw["telemetry"]; ok { var t bool @@ -314,64 +268,6 @@ func GetStrategy() strategy.Strategy { return s } -// GetAgent returns the configured or detected agent. -// Resolution order: -// 1. Explicit agent in settings -// 2. Auto-detect if enabled (default) -// 3. Fall back to default agent -// - -func GetAgent() (agent.Agent, error) { - settings, err := LoadEntireSettings() - if err != nil { - // No settings file, try auto-detect then default - if ag, detectErr := agent.Detect(); detectErr == nil { - return ag, nil - } - return agent.Default(), nil - } - - // Explicit agent configured - if settings.Agent != "" { - ag, err := agent.Get(agent.AgentName(settings.Agent)) - if err != nil { - return nil, fmt.Errorf("getting configured agent: %w", err) - } - return ag, nil - } - - // Auto-detect if enabled (default true for backwards compat) - autoDetect := settings.AgentAutoDetect == nil || *settings.AgentAutoDetect - if autoDetect { - if ag, detectErr := agent.Detect(); detectErr == nil { - return ag, nil - } - } - - // Fall back to default - return agent.Default(), nil -} - -// GetAgentOptions returns options for a specific agent. -// Returns nil if the agent has no options configured. -func GetAgentOptions(agentName string) map[string]interface{} { - settings, err := LoadEntireSettings() - if err != nil { - return nil - } - - if settings.AgentOptions == nil { - return nil - } - - if opts, ok := settings.AgentOptions[agentName]; ok { - if m, ok := opts.(map[string]interface{}); ok { - return m - } - } - return nil -} - // GetLogLevel returns the configured log level from settings. // Returns empty string if not configured (caller should use default). // Note: ENTIRE_LOG_LEVEL env var takes precedence; check it first. @@ -398,3 +294,27 @@ func IsMultiSessionWarningDisabled() bool { } return false } + +// GetAgentsWithHooksInstalled returns names of agents that have hooks installed. +func GetAgentsWithHooksInstalled() []agent.AgentName { + var installed []agent.AgentName + for _, name := range agent.List() { + ag, err := agent.Get(name) + if err != nil { + continue + } + if hs, ok := ag.(agent.HookSupport); ok && hs.AreHooksInstalled() { + installed = append(installed, name) + } + } + return installed +} + +// JoinAgentNames joins agent names into a comma-separated string. +func JoinAgentNames(names []agent.AgentName) string { + strs := make([]string, len(names)) + for i, n := range names { + strs[i] = string(n) + } + return strings.Join(strs, ",") +} diff --git a/cmd/entire/cli/config_test.go b/cmd/entire/cli/config_test.go index 3670c5c7b4..09290f69ff 100644 --- a/cmd/entire/cli/config_test.go +++ b/cmd/entire/cli/config_test.go @@ -5,7 +5,6 @@ import ( "path/filepath" "testing" - "entire.io/cli/cmd/entire/cli/agent" "entire.io/cli/cmd/entire/cli/strategy" ) @@ -333,206 +332,3 @@ func TestLoadEntireSettings_NeitherFileExistsReturnsDefaults(t *testing.T) { t.Error("Enabled should default to true") } } - -func TestGetAgent_NoSettingsFile(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - // Create .claude directory to allow detection - if err := os.MkdirAll(".claude", 0o755); err != nil { - t.Fatalf("Failed to create .claude dir: %v", err) - } - - ag, err := GetAgent() - if err != nil { - t.Fatalf("GetAgent() error = %v", err) - } - if ag == nil { - t.Fatal("GetAgent() returned nil agent") - } - // With .claude directory present, should detect Claude Code - if ag.Name() != agent.AgentNameClaudeCode { - t.Errorf("Expected claude-code agent, got %q", ag.Name()) - } -} - -func TestGetAgent_ExplicitAgent(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - settingsDir := filepath.Dir(EntireSettingsFile) - if err := os.MkdirAll(settingsDir, 0o755); err != nil { - t.Fatalf("Failed to create settings dir: %v", err) - } - - settingsContent := `{"strategy": "manual-commit", "agent": "claude-code"}` - if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil { - t.Fatalf("Failed to write settings file: %v", err) - } - - ag, err := GetAgent() - if err != nil { - t.Fatalf("GetAgent() error = %v", err) - } - if ag.Name() != agent.AgentNameClaudeCode { - t.Errorf("Expected claude-code agent, got %q", ag.Name()) - } -} - -func TestGetAgent_AutoDetectDisabled(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - settingsDir := filepath.Dir(EntireSettingsFile) - if err := os.MkdirAll(settingsDir, 0o755); err != nil { - t.Fatalf("Failed to create settings dir: %v", err) - } - - // Create .claude directory but disable auto-detect - if err := os.MkdirAll(".claude", 0o755); err != nil { - t.Fatalf("Failed to create .claude dir: %v", err) - } - - settingsContent := `{"strategy": "manual-commit", "agent_auto_detect": false}` - if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil { - t.Fatalf("Failed to write settings file: %v", err) - } - - ag, err := GetAgent() - if err != nil { - t.Fatalf("GetAgent() error = %v", err) - } - // Should fall back to default when auto-detect is disabled and no explicit agent - if ag.Name() != agent.DefaultAgentName { - t.Errorf("Expected default agent %q, got %q", agent.DefaultAgentName, ag.Name()) - } -} - -func TestGetAgentOptions_ReturnsOptions(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - settingsDir := filepath.Dir(EntireSettingsFile) - if err := os.MkdirAll(settingsDir, 0o755); err != nil { - t.Fatalf("Failed to create settings dir: %v", err) - } - - settingsContent := `{ - "strategy": "manual-commit", - "agent_options": { - "claude-code": { - "ignore_untracked": true - } - } - }` - if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil { - t.Fatalf("Failed to write settings file: %v", err) - } - - opts := GetAgentOptions("claude-code") - if opts == nil { - t.Fatal("GetAgentOptions() returned nil") - } - if v, ok := opts["ignore_untracked"]; !ok || v != true { - t.Errorf("Expected ignore_untracked=true, got %v", v) - } -} - -func TestGetAgentOptions_ReturnsNilForUnknownAgent(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - settingsDir := filepath.Dir(EntireSettingsFile) - if err := os.MkdirAll(settingsDir, 0o755); err != nil { - t.Fatalf("Failed to create settings dir: %v", err) - } - - settingsContent := `{ - "strategy": "manual-commit", - "agent_options": { - "claude-code": { - "ignore_untracked": true - } - } - }` - if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil { - t.Fatalf("Failed to write settings file: %v", err) - } - - opts := GetAgentOptions("unknown-agent") - if opts != nil { - t.Error("GetAgentOptions() should return nil for unknown agent") - } -} - -func TestGetAgentOptions_ReturnsNilWhenNoSettings(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - opts := GetAgentOptions("claude-code") - if opts != nil { - t.Error("GetAgentOptions() should return nil when no settings file") - } -} - -func TestLoadEntireSettings_AgentFields(t *testing.T) { - tmpDir := t.TempDir() - t.Chdir(tmpDir) - - settingsDir := filepath.Dir(EntireSettingsFile) - if err := os.MkdirAll(settingsDir, 0o755); err != nil { - t.Fatalf("Failed to create settings dir: %v", err) - } - - settingsContent := `{ - "strategy": "manual-commit", - "agent": "claude-code", - "agent_auto_detect": false, - "agent_options": { - "claude-code": {"option1": "value1"} - } - }` - if err := os.WriteFile(EntireSettingsFile, []byte(settingsContent), 0o644); err != nil { - t.Fatalf("Failed to write settings file: %v", err) - } - - settings, err := LoadEntireSettings() - if err != nil { - t.Fatalf("LoadEntireSettings() error = %v", err) - } - - if settings.Agent != "claude-code" { - t.Errorf("Agent should be 'claude-code', got %q", settings.Agent) - } - if settings.AgentAutoDetect == nil || *settings.AgentAutoDetect { - t.Error("AgentAutoDetect should be false") - } - if settings.AgentOptions == nil { - t.Fatal("AgentOptions should not be nil") - } - if _, ok := settings.AgentOptions["claude-code"]; !ok { - t.Error("AgentOptions should have claude-code entry") - } -} - -func TestLoadEntireSettings_LocalOverridesAgent(t *testing.T) { - setupLocalOverrideTestDir(t) - - baseSettings := `{"strategy": "manual-commit", "agent": "cursor"}` - if err := os.WriteFile(EntireSettingsFile, []byte(baseSettings), 0o644); err != nil { - t.Fatalf("Failed to write settings file: %v", err) - } - - localSettings := `{"agent": "claude-code"}` - if err := os.WriteFile(EntireSettingsLocalFile, []byte(localSettings), 0o644); err != nil { - t.Fatalf("Failed to write local settings file: %v", err) - } - - settings, err := LoadEntireSettings() - if err != nil { - t.Fatalf("LoadEntireSettings() error = %v", err) - } - if settings.Agent != "claude-code" { - t.Errorf("Agent should be 'claude-code' from local override, got %q", settings.Agent) - } -} diff --git a/cmd/entire/cli/debug.go b/cmd/entire/cli/debug.go index 6554e83047..3fc276608d 100644 --- a/cmd/entire/cli/debug.go +++ b/cmd/entire/cli/debug.go @@ -358,11 +358,7 @@ func findTranscriptForSession(sessionID, repoRoot string) (string, error) { return "", fmt.Errorf("failed to get agent for type %q: %w", sessionState.AgentType, err) } } else { - // Fall back to auto-detection if no session state - ag, err = GetAgent() - if err != nil { - return "", fmt.Errorf("failed to get agent: %w", err) - } + return "", fmt.Errorf("failed to get agent from sessionID: %s", sessionID) } // Get the session directory for this agent diff --git a/cmd/entire/cli/hook_registry.go b/cmd/entire/cli/hook_registry.go index 52d0885f04..4881335852 100644 --- a/cmd/entire/cli/hook_registry.go +++ b/cmd/entire/cli/hook_registry.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "log/slog" "time" @@ -199,15 +200,15 @@ var currentHookAgentName agent.AgentName // func GetCurrentHookAgent() (agent.Agent, error) { - if currentHookAgentName != "" { - ag, err := agent.Get(currentHookAgentName) - if err != nil { - return nil, fmt.Errorf("getting hook agent %q: %w", currentHookAgentName, err) - } - return ag, nil + if currentHookAgentName == "" { + return nil, errors.New("not in a hook context: agent name not set") + } + + ag, err := agent.Get(currentHookAgentName) + if err != nil { + return nil, fmt.Errorf("getting hook agent %q: %w", currentHookAgentName, err) } - // Fallback for non-hook contexts - return GetAgent() + return ag, nil } // newAgentHooksCmd creates a hooks subcommand for an agent that implements HookHandler. diff --git a/cmd/entire/cli/rewind.go b/cmd/entire/cli/rewind.go index da5a9e5ea7..b5c48c1fee 100644 --- a/cmd/entire/cli/rewind.go +++ b/cmd/entire/cli/rewind.go @@ -29,14 +29,16 @@ import ( // unknownSessionID is the fallback session ID used when no session ID is provided. const unknownSessionID = "unknown" -// getAgentWithFallback returns an agent by type, falling back to detection if type is empty or unknown. -func getAgentWithFallback(agentType agentpkg.AgentType) (agentpkg.Agent, error) { - if agentType != "" { - if agent, err := agentpkg.GetByAgentType(agentType); err == nil { - return agent, nil - } +// getAgent returns an agent by type +func getAgent(agentType agentpkg.AgentType) (agentpkg.Agent, error) { + if agentType == "" { + return nil, errors.New("agent type cannot be empty") + } + ag, err := agentpkg.GetByAgentType(agentType) + if err != nil { + return nil, fmt.Errorf("getting agent: %w", err) } - return GetAgent() + return ag, nil } func newRewindCmd() *cobra.Command { @@ -236,7 +238,7 @@ func runRewindInteractive() error { } // Resolve agent once for use throughout - agent, err := getAgentWithFallback(selectedPoint.Agent) + agent, err := getAgent(selectedPoint.Agent) if err != nil { return fmt.Errorf("failed to get agent: %w", err) } @@ -447,7 +449,7 @@ func runRewindToInternal(commitID string, logsOnly bool, reset bool) error { } // Resolve agent once for use throughout - agent, err := getAgentWithFallback(selectedPoint.Agent) + agent, err := getAgent(selectedPoint.Agent) if err != nil { return fmt.Errorf("failed to get agent: %w", err) } @@ -538,7 +540,7 @@ func runRewindToInternal(commitID string, logsOnly bool, reset bool) error { // Defaults to restoring logs only (no checkout) for safety. func handleLogsOnlyRewindNonInteractive(start strategy.Strategy, point strategy.RewindPoint) error { // Resolve agent once for use throughout - agent, err := getAgentWithFallback(point.Agent) + agent, err := getAgent(point.Agent) if err != nil { return fmt.Errorf("failed to get agent: %w", err) } @@ -580,7 +582,7 @@ func handleLogsOnlyRewindNonInteractive(start strategy.Strategy, point strategy. // This performs a git reset --hard to the target commit. func handleLogsOnlyResetNonInteractive(start strategy.Strategy, point strategy.RewindPoint) error { // Resolve agent once for use throughout - agent, err := getAgentWithFallback(point.Agent) + agent, err := getAgent(point.Agent) if err != nil { return fmt.Errorf("failed to get agent: %w", err) } @@ -883,7 +885,7 @@ func handleLogsOnlyRewindInteractive(start strategy.Strategy, point strategy.Rew // handleLogsOnlyRestore restores only the session logs without changing files. func handleLogsOnlyRestore(start strategy.Strategy, point strategy.RewindPoint) error { // Resolve agent once for use throughout - agent, err := getAgentWithFallback(point.Agent) + agent, err := getAgent(point.Agent) if err != nil { return fmt.Errorf("failed to get agent: %w", err) } @@ -925,7 +927,7 @@ func handleLogsOnlyRestore(start strategy.Strategy, point strategy.RewindPoint) // handleLogsOnlyCheckout restores logs and checks out the commit (detached HEAD). func handleLogsOnlyCheckout(start strategy.Strategy, point strategy.RewindPoint, shortID string) error { // Resolve agent once for use throughout - agent, err := getAgentWithFallback(point.Agent) + agent, err := getAgent(point.Agent) if err != nil { return fmt.Errorf("failed to get agent: %w", err) } @@ -995,7 +997,7 @@ func handleLogsOnlyCheckout(start strategy.Strategy, point strategy.RewindPoint, // handleLogsOnlyReset restores logs and resets the branch to the commit (destructive). func handleLogsOnlyReset(start strategy.Strategy, point strategy.RewindPoint, shortID string) error { // Resolve agent once for use throughout - agent, agentErr := getAgentWithFallback(point.Agent) + agent, agentErr := getAgent(point.Agent) if agentErr != nil { return fmt.Errorf("failed to get agent: %w", agentErr) } diff --git a/cmd/entire/cli/root.go b/cmd/entire/cli/root.go index 49e9a57757..b101a650fd 100644 --- a/cmd/entire/cli/root.go +++ b/cmd/entire/cli/root.go @@ -60,7 +60,9 @@ func NewRootCmd() *cobra.Command { } // Use detached tracking (non-blocking) - telemetry.TrackCommandDetached(cmd, settings.Strategy, settings.Agent, settings.Enabled, Version) + installedAgents := GetAgentsWithHooksInstalled() + agentStr := JoinAgentNames(installedAgents) + telemetry.TrackCommandDetached(cmd, settings.Strategy, agentStr, settings.Enabled, Version) }, RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() diff --git a/cmd/entire/cli/setup.go b/cmd/entire/cli/setup.go index 4960f00f6e..9bee7c6990 100644 --- a/cmd/entire/cli/setup.go +++ b/cmd/entire/cli/setup.go @@ -628,9 +628,8 @@ func setupAgentHooksNonInteractive(agentName agent.AgentName, strategyName strin fmt.Println(msg) } - // Update settings to store the agent choice and strategy + // Update settings to store the strategy settings, _ := LoadEntireSettings() //nolint:errcheck // settings defaults are fine - settings.Agent = string(agentName) settings.Enabled = true if localDev { settings.LocalDev = localDev diff --git a/cmd/entire/cli/setup_test.go b/cmd/entire/cli/setup_test.go index 8d912529d9..49588108d3 100644 --- a/cmd/entire/cli/setup_test.go +++ b/cmd/entire/cli/setup_test.go @@ -407,11 +407,6 @@ func TestRunEnableWithStrategy_PreservesExistingSettings(t *testing.T) { "strategy_options": { "push": true, "some_other_option": "value" - }, - "agent_options": { - "claude-code": { - "ignore_untracked": true - } } }` writeSettings(t, initialSettings) @@ -444,18 +439,6 @@ func TestRunEnableWithStrategy_PreservesExistingSettings(t *testing.T) { if settings.StrategyOptions["some_other_option"] != "value" { t.Errorf("strategy_options.some_other_option should be 'value', got %v", settings.StrategyOptions["some_other_option"]) } - - // agent_options should be preserved - if settings.AgentOptions == nil { - t.Fatal("agent_options should be preserved, but got nil") - } - claudeOpts, ok := settings.AgentOptions["claude-code"].(map[string]interface{}) - if !ok { - t.Fatal("agent_options.claude-code should exist") - } - if claudeOpts["ignore_untracked"] != true { - t.Errorf("agent_options.claude-code.ignore_untracked should be true, got %v", claudeOpts["ignore_untracked"]) - } } func TestRunEnableWithStrategy_PreservesLocalSettings(t *testing.T) { diff --git a/cmd/entire/cli/strategy/manual_commit_rewind.go b/cmd/entire/cli/strategy/manual_commit_rewind.go index 98780182a9..447688bdcb 100644 --- a/cmd/entire/cli/strategy/manual_commit_rewind.go +++ b/cmd/entire/cli/strategy/manual_commit_rewind.go @@ -79,6 +79,7 @@ func (s *ManualCommitStrategy) GetRewindPoints(limit int) ([]RewindPoint, error) ToolUseID: cp.ToolUseID, SessionID: cp.SessionID, SessionPrompt: sessionPrompt, + Agent: state.AgentType, }) } }