diff --git a/cmd/entire/cli/agent/pi/hooks.go b/cmd/entire/cli/agent/pi/hooks.go new file mode 100644 index 000000000..927c6c210 --- /dev/null +++ b/cmd/entire/cli/agent/pi/hooks.go @@ -0,0 +1,211 @@ +package pi + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/entireio/cli/cmd/entire/cli/agent" + "github.com/entireio/cli/cmd/entire/cli/paths" +) + +// Ensure PiAgent implements HookSupport and HookHandler +var ( + _ agent.HookSupport = (*PiAgent)(nil) + _ agent.HookHandler = (*PiAgent)(nil) +) + +// Pi hook names - these become subcommands under `entire hooks pi` +const ( + HookNameSessionStart = "session-start" + HookNameSessionEnd = "session-end" + HookNameStop = "stop" + HookNameUserPromptSubmit = "user-prompt-submit" +) + +// PiSettingsFileName is the settings file used by pi. +const PiSettingsFileName = "settings.json" + +// entireExtensionPackage is the npm package for the pi-entire extension. +const entireExtensionPackage = "npm:@hjanuschka/pi-entire" + +// GetHookNames returns the hook verbs Pi supports. +// These become subcommands: entire hooks pi +func (p *PiAgent) GetHookNames() []string { + return []string{ + HookNameSessionStart, + HookNameSessionEnd, + HookNameStop, + HookNameUserPromptSubmit, + } +} + +// InstallHooks installs pi-entire extension in .pi/settings.json. +// Pi uses extensions rather than external hooks, so we add the extension package. +// Returns the number of changes made. +func (p *PiAgent) InstallHooks(localDev bool, force bool) (int, error) { + repoRoot, err := paths.RepoRoot() + if err != nil { + repoRoot, err = os.Getwd() + if err != nil { + return 0, fmt.Errorf("failed to get current directory: %w", err) + } + } + + settingsPath := filepath.Join(repoRoot, ".pi", PiSettingsFileName) + + // Read existing settings if they exist + // Use map[string]json.RawMessage to preserve unknown fields + var rawSettings map[string]json.RawMessage + var packages []string + + existingData, readErr := os.ReadFile(settingsPath) //nolint:gosec + if readErr == nil { + if err := json.Unmarshal(existingData, &rawSettings); err != nil { + return 0, fmt.Errorf("failed to parse existing settings.json: %w", err) + } + // Extract packages array if it exists + if packagesRaw, ok := rawSettings["packages"]; ok { + if err := json.Unmarshal(packagesRaw, &packages); err != nil { + return 0, fmt.Errorf("failed to parse packages in settings.json: %w", err) + } + } + } else { + rawSettings = make(map[string]json.RawMessage) + } + + // Check if extension is already installed + for _, pkg := range packages { + if pkg == entireExtensionPackage { + return 0, nil // Already installed + } + } + + // Add the extension package + packages = append(packages, entireExtensionPackage) + + // Update the packages field in rawSettings + packagesJSON, err := json.Marshal(packages) + if err != nil { + return 0, fmt.Errorf("failed to marshal packages: %w", err) + } + rawSettings["packages"] = packagesJSON + + // Create directory if needed + if err := os.MkdirAll(filepath.Dir(settingsPath), 0o750); err != nil { + return 0, fmt.Errorf("failed to create .pi directory: %w", err) + } + + // Write settings + output, err := json.MarshalIndent(rawSettings, "", " ") + if err != nil { + return 0, fmt.Errorf("failed to marshal settings: %w", err) + } + + if err := os.WriteFile(settingsPath, output, 0o644); err != nil { + return 0, fmt.Errorf("failed to write settings.json: %w", err) + } + + return 1, nil +} + +// UninstallHooks removes pi-entire extension from settings. +func (p *PiAgent) UninstallHooks() error { + repoRoot, err := paths.RepoRoot() + if err != nil { + repoRoot = "." + } + + settingsPath := filepath.Join(repoRoot, ".pi", PiSettingsFileName) + data, err := os.ReadFile(settingsPath) //nolint:gosec + if err != nil { + return nil // No settings file means nothing to uninstall + } + + // Use map[string]json.RawMessage to preserve unknown fields + var rawSettings map[string]json.RawMessage + if err := json.Unmarshal(data, &rawSettings); err != nil { + return fmt.Errorf("failed to parse settings.json: %w", err) + } + + // Extract and modify packages array + var packages []string + if packagesRaw, ok := rawSettings["packages"]; ok { + if err := json.Unmarshal(packagesRaw, &packages); err != nil { + return fmt.Errorf("failed to parse packages in settings.json: %w", err) + } + } + + // Remove the extension package + newPackages := make([]string, 0, len(packages)) + for _, pkg := range packages { + if pkg != entireExtensionPackage { + newPackages = append(newPackages, pkg) + } + } + + // Update the packages field in rawSettings + packagesJSON, err := json.Marshal(newPackages) + if err != nil { + return fmt.Errorf("failed to marshal packages: %w", err) + } + rawSettings["packages"] = packagesJSON + + // Write back + output, err := json.MarshalIndent(rawSettings, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal settings: %w", err) + } + + if err := os.WriteFile(settingsPath, output, 0o644); err != nil { + return fmt.Errorf("failed to write settings.json: %w", err) + } + + return nil +} + +// AreHooksInstalled checks if pi-entire extension is installed. +func (p *PiAgent) AreHooksInstalled() bool { + repoRoot, err := paths.RepoRoot() + if err != nil { + repoRoot = "." + } + + settingsPath := filepath.Join(repoRoot, ".pi", PiSettingsFileName) + data, err := os.ReadFile(settingsPath) //nolint:gosec + if err != nil { + return false + } + + // Use map[string]json.RawMessage for consistency + var rawSettings map[string]json.RawMessage + if err := json.Unmarshal(data, &rawSettings); err != nil { + return false + } + + var packages []string + if packagesRaw, ok := rawSettings["packages"]; ok { + if err := json.Unmarshal(packagesRaw, &packages); err != nil { + return false + } + } + + for _, pkg := range packages { + if pkg == entireExtensionPackage { + return true + } + } + + return false +} + +// GetSupportedHooks returns the hook types Pi supports. +func (p *PiAgent) GetSupportedHooks() []agent.HookType { + return []agent.HookType{ + agent.HookSessionStart, + agent.HookSessionEnd, + agent.HookUserPromptSubmit, + agent.HookStop, + } +} diff --git a/cmd/entire/cli/agent/pi/pi.go b/cmd/entire/cli/agent/pi/pi.go new file mode 100644 index 000000000..d2b116a86 --- /dev/null +++ b/cmd/entire/cli/agent/pi/pi.go @@ -0,0 +1,539 @@ +// Package pi implements the Agent interface for pi coding agent. +package pi + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/entireio/cli/cmd/entire/cli/agent" + "github.com/entireio/cli/cmd/entire/cli/paths" +) + +//nolint:gochecknoinits // Agent self-registration is the intended pattern +func init() { + agent.Register(agent.AgentNamePi, NewPiAgent) +} + +// PiAgent implements the Agent interface for pi coding agent. +type PiAgent struct{} + +// NewPiAgent creates a new pi agent instance. +func NewPiAgent() agent.Agent { + return &PiAgent{} +} + +// Name returns the agent registry key. +func (p *PiAgent) Name() agent.AgentName { + return agent.AgentNamePi +} + +// Type returns the agent type identifier. +func (p *PiAgent) Type() agent.AgentType { + return agent.AgentTypePi +} + +// Description returns a human-readable description. +func (p *PiAgent) Description() string { + return "Pi - AI coding agent CLI" +} + +// DetectPresence checks if pi is configured in the repository. +func (p *PiAgent) DetectPresence() (bool, error) { + repoRoot, err := paths.RepoRoot() + if err != nil { + repoRoot = "." + } + + // Check for .pi directory + piDir := filepath.Join(repoRoot, ".pi") + if _, err := os.Stat(piDir); err == nil { + return true, nil + } + + return false, nil +} + +// GetHookConfigPath returns the path to pi's settings file. +// Pi uses extensions rather than hook config, so this returns empty. +func (p *PiAgent) GetHookConfigPath() string { + return "" +} + +// SupportsHooks returns true as pi supports lifecycle events via extensions. +func (p *PiAgent) SupportsHooks() bool { + return true +} + +// ParseHookInput parses pi hook input from stdin. +func (p *PiAgent) ParseHookInput(hookType agent.HookType, reader io.Reader) (*agent.HookInput, error) { + data, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("failed to read input: %w", err) + } + + if len(data) == 0 { + return nil, errors.New("empty input") + } + + input := &agent.HookInput{ + HookType: hookType, + Timestamp: time.Now(), + RawData: make(map[string]interface{}), + } + + // Parse the JSON input + var raw piHookInput + if err := json.Unmarshal(data, &raw); err != nil { + return nil, fmt.Errorf("failed to parse hook input: %w", err) + } + + input.SessionID = raw.SessionID + input.SessionRef = raw.TranscriptPath + input.UserPrompt = raw.Prompt + + // Store modified files in RawData + if len(raw.ModifiedFiles) > 0 { + input.RawData["modified_files"] = raw.ModifiedFiles + } + + return input, nil +} + +// GetSessionID extracts the session ID from hook input. +func (p *PiAgent) GetSessionID(input *agent.HookInput) string { + return input.SessionID +} + +// TransformSessionID converts a pi session ID to an Entire session ID. +// Pi uses UUIDs, which we use directly. +func (p *PiAgent) TransformSessionID(agentSessionID string) string { + return agentSessionID +} + +// ExtractAgentSessionID extracts the pi session ID from an Entire session ID. +func (p *PiAgent) ExtractAgentSessionID(entireSessionID string) string { + return entireSessionID +} + +// GetSessionDir returns the directory where pi stores session transcripts. +func (p *PiAgent) GetSessionDir(repoPath string) (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %w", err) + } + + // Pi stores sessions in ~/.pi/agent/sessions/----/ + sanitizedPath := sanitizePathForPi(repoPath) + return filepath.Join(homeDir, ".pi", "agent", "sessions", sanitizedPath), nil +} + +// sanitizePathForPi converts a path to pi's session directory format. +// Pi replaces / with - and adds -- prefix/suffix. +func sanitizePathForPi(path string) string { + // Replace path separators with dashes + sanitized := strings.ReplaceAll(path, "/", "-") + sanitized = strings.ReplaceAll(sanitized, "\\", "-") + return "--" + sanitized + "--" +} + +// ReadSession reads a session from pi's storage (JSONL transcript file). +func (p *PiAgent) ReadSession(input *agent.HookInput) (*agent.AgentSession, error) { + if input.SessionRef == "" { + return nil, errors.New("session reference (transcript path) is required") + } + + // Read the raw JSONL file + data, err := os.ReadFile(input.SessionRef) + if err != nil { + return nil, fmt.Errorf("failed to read transcript: %w", err) + } + + // Parse to extract computed fields + modifiedFiles := p.extractModifiedFiles(data) + + return &agent.AgentSession{ + SessionID: input.SessionID, + AgentName: p.Name(), + SessionRef: input.SessionRef, + StartTime: time.Now(), + NativeData: data, + ModifiedFiles: modifiedFiles, + }, nil +} + +// WriteSession writes a session to pi's storage. +func (p *PiAgent) WriteSession(session *agent.AgentSession) error { + if session == nil { + return errors.New("session is nil") + } + + if session.AgentName != "" && session.AgentName != p.Name() { + return fmt.Errorf("session belongs to agent %q, not %q", session.AgentName, p.Name()) + } + + if session.SessionRef == "" { + return errors.New("session reference (transcript path) is required") + } + + if len(session.NativeData) == 0 { + return errors.New("session has no native data to write") + } + + if err := os.WriteFile(session.SessionRef, session.NativeData, 0o600); err != nil { + return fmt.Errorf("failed to write transcript: %w", err) + } + + return nil +} + +// FormatResumeCommand returns the command to resume a pi session. +func (p *PiAgent) FormatResumeCommand(sessionID string) string { + return "pi # then use /resume to select session" +} + +// GetLastUserPrompt extracts the last user prompt from the session. +func (p *PiAgent) GetLastUserPrompt(session *agent.AgentSession) string { + if session == nil || len(session.NativeData) == 0 { + return "" + } + + scanner := bufio.NewScanner(strings.NewReader(string(session.NativeData))) + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + var lastPrompt string + + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + + var entry piSessionEntry + if err := json.Unmarshal(line, &entry); err != nil { + continue + } + + if entry.Type == "message" && entry.Message != nil && entry.Message.Role == "user" { + // Extract text from content + for _, block := range entry.Message.Content { + if block.Type == "text" && block.Text != "" { + lastPrompt = block.Text + } + } + } + } + + return lastPrompt +} + +// TruncateAtUUID returns a new session truncated at the given entry ID (inclusive). +func (p *PiAgent) TruncateAtUUID(session *agent.AgentSession, entryID string) (*agent.AgentSession, error) { + if session == nil { + return nil, errors.New("session is nil") + } + + if len(session.NativeData) == 0 { + return nil, errors.New("session has no native data") + } + + if entryID == "" { + // No truncation needed, return copy + return &agent.AgentSession{ + SessionID: session.SessionID, + AgentName: session.AgentName, + RepoPath: session.RepoPath, + SessionRef: session.SessionRef, + StartTime: session.StartTime, + NativeData: session.NativeData, + ModifiedFiles: session.ModifiedFiles, + }, nil + } + + // Parse and truncate + var result []byte + scanner := bufio.NewScanner(strings.NewReader(string(session.NativeData))) + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + + // Add this line to result + result = append(result, line...) + result = append(result, '\n') + + // Check if this is the target entry + var entry piSessionEntry + if err := json.Unmarshal(line, &entry); err == nil { + if entry.ID == entryID { + break + } + } + } + + return &agent.AgentSession{ + SessionID: session.SessionID, + AgentName: session.AgentName, + RepoPath: session.RepoPath, + SessionRef: session.SessionRef, + StartTime: session.StartTime, + NativeData: result, + ModifiedFiles: p.extractModifiedFiles(result), + }, nil +} + +// FindCheckpointUUID finds the entry ID of the message containing the tool result +// for the given tool call ID. +func (p *PiAgent) FindCheckpointUUID(session *agent.AgentSession, toolCallID string) (string, bool) { + if session == nil || len(session.NativeData) == 0 { + return "", false + } + + scanner := bufio.NewScanner(strings.NewReader(string(session.NativeData))) + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + + var entry piSessionEntry + if err := json.Unmarshal(line, &entry); err != nil { + continue + } + + if entry.Type == "message" && entry.Message != nil && entry.Message.Role == "toolResult" { + if entry.Message.ToolCallID == toolCallID { + return entry.ID, true + } + } + } + + return "", false +} + +// CalculateTokenUsage calculates token usage from a pi transcript. +// Pi stores usage in assistant messages. +func (p *PiAgent) CalculateTokenUsage(transcript []byte) *agent.TokenUsage { + usage := &agent.TokenUsage{} + + scanner := bufio.NewScanner(strings.NewReader(string(transcript))) + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + + var entry piSessionEntry + if err := json.Unmarshal(line, &entry); err != nil { + continue + } + + if entry.Type == "message" && entry.Message != nil && entry.Message.Role == "assistant" { + // Pi stores usage in the message - we'd need to parse it + // For now, count API calls + usage.APICallCount++ + } + } + + return usage +} + +// extractModifiedFiles parses JSONL and extracts modified file paths. +func (p *PiAgent) extractModifiedFiles(data []byte) []string { + files := make(map[string]bool) + + scanner := bufio.NewScanner(strings.NewReader(string(data))) + // Increase buffer for large lines + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + + var entry piSessionEntry + if err := json.Unmarshal(line, &entry); err != nil { + continue + } + + // Check for tool results with file paths + if entry.Type == "message" && entry.Message != nil { + msg := entry.Message + + // Check toolResult messages for write/edit + if msg.Role == "toolResult" { + if msg.ToolName == "write" || msg.ToolName == "edit" { + if details, ok := msg.Details.(map[string]interface{}); ok { + if path, ok := details["path"].(string); ok && path != "" { + files[path] = true + } + } + } + } + + // Check assistant messages for tool calls + if msg.Role == "assistant" { + for _, content := range msg.Content { + if content.Type == "toolCall" { + if content.Name == "write" || content.Name == "edit" { + if args, ok := content.Arguments.(map[string]interface{}); ok { + if path, ok := args["path"].(string); ok && path != "" { + files[path] = true + } + } + } + } + } + } + } + } + + result := make([]string, 0, len(files)) + for f := range files { + result = append(result, f) + } + return result +} + +// TranscriptAnalyzer interface implementation + +// GetTranscriptPosition returns the current line count of a pi transcript. +func (p *PiAgent) GetTranscriptPosition(path string) (int, error) { + if path == "" { + return 0, nil + } + + file, err := os.Open(path) //nolint:gosec + if err != nil { + if os.IsNotExist(err) { + return 0, nil + } + return 0, fmt.Errorf("failed to open transcript file: %w", err) + } + defer file.Close() + + reader := bufio.NewReader(file) + lineCount := 0 + + for { + lineData, err := reader.ReadBytes('\n') + if len(lineData) > 0 { + lineCount++ + } + if err != nil { + if err == io.EOF { + break + } + return 0, fmt.Errorf("failed to read transcript: %w", err) + } + } + + return lineCount, nil +} + +// ExtractModifiedFilesFromOffset extracts files modified since a given line number. +func (p *PiAgent) ExtractModifiedFilesFromOffset(path string, startOffset int) (files []string, currentPosition int, err error) { + if path == "" { + return nil, 0, nil + } + + file, openErr := os.Open(path) //nolint:gosec + if openErr != nil { + return nil, 0, fmt.Errorf("failed to open transcript file: %w", openErr) + } + defer file.Close() + + reader := bufio.NewReader(file) + modFiles := make(map[string]bool) + lineNum := 0 + + for { + lineData, readErr := reader.ReadBytes('\n') + if readErr != nil && readErr != io.EOF { + return nil, 0, fmt.Errorf("failed to read transcript: %w", readErr) + } + + if len(lineData) > 0 { + lineNum++ + if lineNum > startOffset { + var entry piSessionEntry + if parseErr := json.Unmarshal(lineData, &entry); parseErr == nil { + // Extract files from this entry + if entry.Type == "message" && entry.Message != nil { + msg := entry.Message + + // Check toolResult messages for write/edit + if msg.Role == "toolResult" && (msg.ToolName == "write" || msg.ToolName == "edit") { + if details, ok := msg.Details.(map[string]interface{}); ok { + if filePath, ok := details["path"].(string); ok && filePath != "" { + modFiles[filePath] = true + } + } + } + + // Check assistant messages for tool calls + if msg.Role == "assistant" { + for _, content := range msg.Content { + if content.Type == "toolCall" { + if content.Name == "write" || content.Name == "edit" { + if args, ok := content.Arguments.(map[string]interface{}); ok { + if filePath, ok := args["path"].(string); ok && filePath != "" { + modFiles[filePath] = true + } + } + } + } + } + } + } + } + } + } + + if readErr == io.EOF { + break + } + } + + result := make([]string, 0, len(modFiles)) + for f := range modFiles { + result = append(result, f) + } + + return result, lineNum, nil +} + +// TranscriptChunker interface implementation + +// ChunkTranscript splits a JSONL transcript at line boundaries. +func (p *PiAgent) ChunkTranscript(content []byte, maxSize int) ([][]byte, error) { + chunks, err := agent.ChunkJSONL(content, maxSize) + if err != nil { + return nil, fmt.Errorf("failed to chunk JSONL transcript: %w", err) + } + return chunks, nil +} + +// ReassembleTranscript concatenates JSONL chunks. +func (p *PiAgent) ReassembleTranscript(chunks [][]byte) ([]byte, error) { + return agent.ReassembleJSONL(chunks), nil +} diff --git a/cmd/entire/cli/agent/pi/types.go b/cmd/entire/cli/agent/pi/types.go new file mode 100644 index 000000000..8bb67c64a --- /dev/null +++ b/cmd/entire/cli/agent/pi/types.go @@ -0,0 +1,60 @@ +package pi + +// piHookInput represents the JSON input from pi extension hooks. +type piHookInput struct { + SessionID string `json:"session_id"` + TranscriptPath string `json:"transcript_path"` + Prompt string `json:"prompt,omitempty"` + ModifiedFiles []string `json:"modified_files,omitempty"` +} + +// piSessionEntry represents a line in pi's JSONL session file. +type piSessionEntry struct { + Type string `json:"type"` + ID string `json:"id,omitempty"` + ParentID string `json:"parentId,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Message *piMessage `json:"message,omitempty"` + Version int `json:"version,omitempty"` + CWD string `json:"cwd,omitempty"` + Summary string `json:"summary,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +// piMessage represents a message in pi's session format. +type piMessage struct { + Role string `json:"role"` + Content []piContentBlock `json:"content,omitempty"` + ToolName string `json:"toolName,omitempty"` + ToolCallID string `json:"toolCallId,omitempty"` + Details interface{} `json:"details,omitempty"` + IsError bool `json:"isError,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + Provider string `json:"provider,omitempty"` + Model string `json:"model,omitempty"` +} + +// piContentBlock represents a content block in pi messages. +type piContentBlock struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Arguments interface{} `json:"arguments,omitempty"` +} + +// PiSettings represents pi's .pi/settings.json format. +type PiSettings struct { + Packages []string `json:"packages,omitempty"` + Extensions []string `json:"extensions,omitempty"` +} + +// PiSessionHeader represents the first line of a pi session file. +type PiSessionHeader struct { + Type string `json:"type"` // "session" + Version int `json:"version"` + ID string `json:"id"` + Timestamp string `json:"timestamp"` + CWD string `json:"cwd"` + ParentSession string `json:"parentSession,omitempty"` +} diff --git a/cmd/entire/cli/agent/registry.go b/cmd/entire/cli/agent/registry.go index 901300196..638d5c425 100644 --- a/cmd/entire/cli/agent/registry.go +++ b/cmd/entire/cli/agent/registry.go @@ -80,12 +80,14 @@ type AgentType string const ( AgentNameClaudeCode AgentName = "claude-code" AgentNameGemini AgentName = "gemini" + AgentNamePi AgentName = "pi" ) // Agent type constants (type identifiers stored in metadata/trailers) const ( AgentTypeClaudeCode AgentType = "Claude Code" AgentTypeGemini AgentType = "Gemini CLI" + AgentTypePi AgentType = "Pi Coding Agent" AgentTypeUnknown AgentType = "Agent" // Fallback for backwards compatibility ) diff --git a/cmd/entire/cli/config.go b/cmd/entire/cli/config.go index 48246c5be..6394e6162 100644 --- a/cmd/entire/cli/config.go +++ b/cmd/entire/cli/config.go @@ -11,8 +11,10 @@ import ( "github.com/entireio/cli/cmd/entire/cli/settings" "github.com/entireio/cli/cmd/entire/cli/strategy" - // Import claudecode to register the agent + // Import agents to register them _ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode" + _ "github.com/entireio/cli/cmd/entire/cli/agent/geminicli" + _ "github.com/entireio/cli/cmd/entire/cli/agent/pi" ) // Package-level aliases to avoid shadowing the settings package with local variables named "settings". diff --git a/cmd/entire/cli/hooks_cmd.go b/cmd/entire/cli/hooks_cmd.go index d12922523..e03696d35 100644 --- a/cmd/entire/cli/hooks_cmd.go +++ b/cmd/entire/cli/hooks_cmd.go @@ -5,6 +5,7 @@ import ( // Import agents to ensure they are registered before we iterate _ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode" _ "github.com/entireio/cli/cmd/entire/cli/agent/geminicli" + _ "github.com/entireio/cli/cmd/entire/cli/agent/pi" "github.com/spf13/cobra" )