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
13 changes: 8 additions & 5 deletions container/internal/claude/parser/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/vanpelt/catnip/internal/models"
)

// HistoryReader reads user prompt history from Claude configuration files
// Supports both legacy ~/.claude.json and new ~/.claude/history.jsonl formats
// Supports both legacy ~/.claude.json and new <claude-config-dir>/history.jsonl formats
type HistoryReader struct {
claudeConfigPath string
historyJSONLPath string
}

// NewHistoryReader creates a new history reader with paths to config files
func NewHistoryReader(homeDir string) *HistoryReader {
// NewHistoryReader creates a new history reader with paths to config files.
// claudeConfigDir should be obtained from config.Runtime.ClaudeConfigDir after
// runtime initialization (typically passed from the caller).
func NewHistoryReader(homeDir, claudeConfigDir string) *HistoryReader {
return &HistoryReader{
claudeConfigPath: homeDir + "/.claude.json",
historyJSONLPath: homeDir + "/.claude/history.jsonl",
claudeConfigPath: filepath.Join(homeDir, ".claude.json"),
historyJSONLPath: filepath.Join(claudeConfigDir, "history.jsonl"),
}
}

Expand Down
13 changes: 6 additions & 7 deletions container/internal/claude/paths/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"path/filepath"
"sort"
"strings"

"github.com/vanpelt/catnip/internal/config"
)

// EncodePathForClaude encodes a filesystem path the way Claude does for project directories.
Expand All @@ -23,15 +25,12 @@ func EncodePathForClaude(path string) string {
}

// GetProjectDir returns the Claude projects directory path for a given worktree/project path.
// Returns the full path to ~/.claude/projects/<encoded-path>/
// Returns the full path to <claude-config-dir>/projects/<encoded-path>/
// On Linux, respects XDG_CONFIG_HOME (defaults to ~/.config/claude/projects).
// On macOS and other platforms, uses ~/.claude/projects.
func GetProjectDir(worktreePath string) (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}

encodedPath := EncodePathForClaude(worktreePath)
return filepath.Join(homeDir, ".claude", "projects", encodedPath), nil
return filepath.Join(config.Runtime.GetClaudeProjectsDir(), encodedPath), nil
}

// IsValidSessionUUID checks if a string is a valid Claude session UUID.
Expand Down
14 changes: 5 additions & 9 deletions container/internal/cmd/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/vanpelt/catnip/internal/config"
"github.com/vanpelt/catnip/internal/logger"
)

Expand Down Expand Up @@ -106,18 +107,13 @@ func runInstallHooks(cmd *cobra.Command, args []string) error {
logger.Info("🔧 Installing Claude Code hooks for activity tracking...")
}

// Get Claude directory
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}

claudeDir := filepath.Join(homeDir, ".claude")
// Get Claude directory (respects XDG_CONFIG_HOME on Linux)
claudeDir := config.Runtime.ClaudeConfigDir
settingsFile := filepath.Join(claudeDir, "settings.json")

// Create .claude directory if it doesn't exist
// Create Claude config directory if it doesn't exist
if err := os.MkdirAll(claudeDir, 0755); err != nil {
return fmt.Errorf("failed to create .claude directory: %w", err)
return fmt.Errorf("failed to create Claude config directory: %w", err)
}

if verboseHooks {
Expand Down
21 changes: 21 additions & 0 deletions container/internal/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type RuntimeConfig struct {
LiveDir string
HomeDir string
TempDir string
ClaudeConfigDir string // Claude config directory (respects XDG_CONFIG_HOME on Linux)
CurrentRepo string // For native mode, the git repo we're running from
SyncEnabled bool // Whether to sync settings to volume
PortMonitorEnabled bool // Whether to use /proc for port monitoring
Expand All @@ -53,6 +54,18 @@ func getEnvOrDefault(key, defaultValue string) string {
return defaultValue
}

// getClaudeConfigDir returns the Claude config directory path.
// If XDG_CONFIG_HOME is set, uses $XDG_CONFIG_HOME/claude.
// Otherwise, uses ~/.claude (the traditional Claude location).
func getClaudeConfigDir(homeDir string) string {
// Check XDG_CONFIG_HOME first (cross-platform)
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
return filepath.Join(xdgConfigHome, "claude")
}
// Default to ~/.claude when XDG_CONFIG_HOME is not set
return filepath.Join(homeDir, ".claude")
}

// DetectRuntime determines the current runtime environment and returns appropriate configuration
func DetectRuntime() *RuntimeConfig {
mode := detectMode()
Expand All @@ -78,6 +91,7 @@ func DetectRuntime() *RuntimeConfig {
config.LiveDir = getEnvOrDefault("CATNIP_LIVE_DIR", "/live")
config.HomeDir = getEnvOrDefault("CATNIP_HOME_DIR", "/home/catnip")
config.TempDir = getEnvOrDefault("CATNIP_TEMP_DIR", "/tmp")
config.ClaudeConfigDir = getClaudeConfigDir(config.HomeDir)
config.SyncEnabled = true
config.PortMonitorEnabled = true

Expand All @@ -90,6 +104,7 @@ func DetectRuntime() *RuntimeConfig {
config.LiveDir = getEnvOrDefault("CATNIP_LIVE_DIR", "") // Will be set if running from a git repo
config.HomeDir = getEnvOrDefault("CATNIP_HOME_DIR", homeDir)
config.TempDir = getEnvOrDefault("CATNIP_TEMP_DIR", os.TempDir())
config.ClaudeConfigDir = getClaudeConfigDir(config.HomeDir)
config.SyncEnabled = false // No need to sync in native mode
config.PortMonitorEnabled = runtime.GOOS == "linux" // Only on Linux

Expand Down Expand Up @@ -270,3 +285,9 @@ func (rc *RuntimeConfig) IsNative() bool {
func (rc *RuntimeConfig) IsContainerized() bool {
return rc.Mode == DockerMode || rc.Mode == ContainerMode
}

// GetClaudeProjectsDir returns the Claude projects directory path.
// This is ClaudeConfigDir + "/projects"
func (rc *RuntimeConfig) GetClaudeProjectsDir() string {
return filepath.Join(rc.ClaudeConfigDir, "projects")
}
36 changes: 36 additions & 0 deletions container/internal/config/runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package config

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetClaudeConfigDir(t *testing.T) {
homeDir := "/home/testuser"

t.Run("with XDG_CONFIG_HOME set uses $XDG_CONFIG_HOME/claude", func(t *testing.T) {
// t.Setenv automatically restores the original value after the test
t.Setenv("XDG_CONFIG_HOME", "/custom/config")
result := getClaudeConfigDir(homeDir)
assert.Equal(t, "/custom/config/claude", result)
})

t.Run("without XDG_CONFIG_HOME uses ~/.claude", func(t *testing.T) {
// t.Setenv with empty string effectively unsets for our purposes
t.Setenv("XDG_CONFIG_HOME", "")
result := getClaudeConfigDir(homeDir)
assert.Equal(t, filepath.Join(homeDir, ".claude"), result)
})
}

func TestGetClaudeProjectsDir(t *testing.T) {
t.Run("returns ClaudeConfigDir/projects", func(t *testing.T) {
rc := &RuntimeConfig{
ClaudeConfigDir: "/home/user/.config/claude",
}
result := rc.GetClaudeProjectsDir()
assert.Equal(t, "/home/user/.config/claude/projects", result)
})
}
Loading
Loading