diff --git a/cmd/entire/cli/agent/agent.go b/cmd/entire/cli/agent/agent.go index 68473284b..94c74f6d7 100644 --- a/cmd/entire/cli/agent/agent.go +++ b/cmd/entire/cli/agent/agent.go @@ -11,12 +11,10 @@ import ( // Each agent implementation (Claude Code, Cursor, Aider, etc.) converts its // native format to the normalized types defined in this package. // -// The interface is organized into four groups: -// +// The interface is organized into three groups: // - Identity (5 methods): Name, Type, Description, DetectPresence, ProtectedDirs -// - Event Mapping (2 methods): HookNames, ParseHookEvent // - Transcript Storage (3 methods): ReadTranscript, ChunkTranscript, ReassembleTranscript -// - Legacy (8 methods): Will be moved to optional interfaces or removed in a future phase +// - Legacy (6 methods): Will be moved to optional interfaces or removed in a future phase type Agent interface { // --- Identity --- @@ -41,18 +39,6 @@ type Agent interface { // Examples: [".claude"] for Claude, [".gemini"] for Gemini. ProtectedDirs() []string - // --- Event Mapping --- - - // HookNames returns the hook verbs this agent supports. - // These become subcommands under `entire hooks `. - // e.g., ["stop", "user-prompt-submit", "session-start", "session-end"] - HookNames() []string - - // ParseHookEvent translates an agent-native hook into a normalized lifecycle Event. - // Returns nil if the hook has no lifecycle significance (e.g., pass-through hooks). - // This is the core contribution surface for new agent implementations. - ParseHookEvent(hookName string, stdin io.Reader) (*Event, error) - // --- Transcript Storage --- // ReadTranscript reads the raw transcript bytes for a session. @@ -91,9 +77,23 @@ type Agent interface { // HookSupport is implemented by agents with lifecycle hooks. // This optional interface allows agents like Claude Code and Cursor to // install and manage hooks that notify Entire of agent events. +// +// The interface is organized into two groups: +// - Hook Mapping (2 methods): HookNames, ParseHookEvent +// - Hook Management (3 methods): InstallHooks, UninstallHooks, AreHooksInstalled type HookSupport interface { Agent + // HookNames returns the hook verbs this agent supports. + // These become subcommands under `entire hooks `. + // e.g., ["stop", "user-prompt-submit", "session-start", "session-end"] + HookNames() []string + + // ParseHookEvent translates an agent-native hook into a normalized lifecycle Event. + // Returns nil if the hook has no lifecycle significance (e.g., pass-through hooks). + // This is the core contribution surface for new agent implementations. + ParseHookEvent(hookName string, stdin io.Reader) (*Event, error) + // InstallHooks installs agent-specific hooks. // If localDev is true, hooks point to local development build. // If force is true, removes existing Entire hooks before installing. diff --git a/cmd/entire/cli/agent/agent_test.go b/cmd/entire/cli/agent/agent_test.go index 67a0eda59..fdf204bb4 100644 --- a/cmd/entire/cli/agent/agent_test.go +++ b/cmd/entire/cli/agent/agent_test.go @@ -21,11 +21,8 @@ func (m *mockAgent) DetectPresence() (bool, error) { return false, nil } func (m *mockAgent) GetSessionID(_ *HookInput) string { return "" } func (m *mockAgent) ProtectedDirs() []string { return nil } -func (m *mockAgent) HookNames() []string { return nil } -//nolint:nilnil // Mock implementation -func (m *mockAgent) ParseHookEvent(_ string, _ io.Reader) (*Event, error) { return nil, nil } -func (m *mockAgent) ReadTranscript(_ string) ([]byte, error) { return nil, nil } +func (m *mockAgent) ReadTranscript(_ string) ([]byte, error) { return nil, nil } func (m *mockAgent) ChunkTranscript(content []byte, _ int) ([][]byte, error) { return [][]byte{content}, nil } @@ -53,9 +50,13 @@ type mockHookSupport struct { var _ HookSupport = (*mockHookSupport)(nil) // Compile-time interface check -func (m *mockHookSupport) InstallHooks(_, _ bool) (int, error) { return 0, nil } -func (m *mockHookSupport) UninstallHooks() error { return nil } -func (m *mockHookSupport) AreHooksInstalled() bool { return false } +func (m *mockHookSupport) HookNames() []string { return nil } + +//nolint:nilnil // Mock implementation +func (m *mockHookSupport) ParseHookEvent(_ string, _ io.Reader) (*Event, error) { return nil, nil } +func (m *mockHookSupport) InstallHooks(_, _ bool) (int, error) { return 0, nil } +func (m *mockHookSupport) UninstallHooks() error { return nil } +func (m *mockHookSupport) AreHooksInstalled() bool { return false } // mockFileWatcher implements both Agent and FileWatcher interfaces. type mockFileWatcher struct { diff --git a/cmd/entire/cli/hook_registry.go b/cmd/entire/cli/hook_registry.go index 2d3abc0e5..a24591a28 100644 --- a/cmd/entire/cli/hook_registry.go +++ b/cmd/entire/cli/hook_registry.go @@ -44,9 +44,9 @@ func GetCurrentHookAgent() (agent.Agent, error) { return ag, nil } -// newAgentHooksCmd creates a hooks subcommand for an agent. +// newAgentHooksCmd creates a hooks subcommand for an agent that implements HookSupport. // It dynamically creates subcommands for each hook the agent supports. -func newAgentHooksCmd(agentName agent.AgentName, handler agent.Agent) *cobra.Command { +func newAgentHooksCmd(agentName agent.AgentName, handler agent.HookSupport) *cobra.Command { cmd := &cobra.Command{ Use: string(agentName), Short: handler.Description() + " hook handlers", @@ -132,8 +132,13 @@ func newAgentHookVerbCmdWithLogging(agentName agent.AgentName, hookName string) return fmt.Errorf("failed to get agent %q: %w", agentName, agentErr) } + handler, ok := ag.(agent.HookSupport) + if !ok { + return fmt.Errorf("agent %q does not support hooks", agentName) + } + // Use cmd.InOrStdin() to support testing with cmd.SetIn() - event, parseErr := ag.ParseHookEvent(hookName, cmd.InOrStdin()) + event, parseErr := handler.ParseHookEvent(hookName, cmd.InOrStdin()) if parseErr != nil { return fmt.Errorf("failed to parse hook event: %w", parseErr) } diff --git a/cmd/entire/cli/hooks_cmd.go b/cmd/entire/cli/hooks_cmd.go index 215954a56..fbec53e3b 100644 --- a/cmd/entire/cli/hooks_cmd.go +++ b/cmd/entire/cli/hooks_cmd.go @@ -22,12 +22,15 @@ func newHooksCmd() *cobra.Command { cmd.AddCommand(newHooksGitCmd()) // Dynamically add agent hook subcommands + // Each agent that implements HookSupport gets its own subcommand tree for _, agentName := range agent.List() { ag, err := agent.Get(agentName) if err != nil { continue } - cmd.AddCommand(newAgentHooksCmd(agentName, ag)) + if handler, ok := ag.(agent.HookSupport); ok { + cmd.AddCommand(newAgentHooksCmd(agentName, handler)) + } } return cmd diff --git a/cmd/entire/cli/lifecycle_test.go b/cmd/entire/cli/lifecycle_test.go index cc03dd98f..a4645f331 100644 --- a/cmd/entire/cli/lifecycle_test.go +++ b/cmd/entire/cli/lifecycle_test.go @@ -2,7 +2,6 @@ package cli import ( "errors" - "io" "os" "path/filepath" "strings" @@ -32,14 +31,8 @@ func (m *mockLifecycleAgent) Description() string { return "M func (m *mockLifecycleAgent) IsPreview() bool { return false } func (m *mockLifecycleAgent) DetectPresence() (bool, error) { return false, nil } func (m *mockLifecycleAgent) ProtectedDirs() []string { return nil } -func (m *mockLifecycleAgent) HookNames() []string { return nil } func (m *mockLifecycleAgent) GetSessionID(_ *agent.HookInput) string { return "" } -//nolint:nilnil // Mock implementation -func (m *mockLifecycleAgent) ParseHookEvent(_ string, _ io.Reader) (*agent.Event, error) { - return nil, nil -} - func (m *mockLifecycleAgent) ReadTranscript(_ string) ([]byte, error) { if m.transcriptErr != nil { return nil, m.transcriptErr