Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions cmd/entire/cli/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand All @@ -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 <agent>`.
// 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.
Expand Down Expand Up @@ -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 <agent>`.
// 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.
Expand Down
15 changes: 8 additions & 7 deletions cmd/entire/cli/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 8 additions & 3 deletions cmd/entire/cli/hook_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/entire/cli/hooks_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 0 additions & 7 deletions cmd/entire/cli/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cli

import (
"errors"
"io"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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
Expand Down