diff --git a/cmd/picoclaw-launcher-tui/internal/config/store.go b/cmd/picoclaw-launcher-tui/internal/config/store.go index e726ae3b7..d16938e21 100644 --- a/cmd/picoclaw-launcher-tui/internal/config/store.go +++ b/cmd/picoclaw-launcher-tui/internal/config/store.go @@ -2,14 +2,13 @@ package configstore import ( "errors" - "os" "path/filepath" picoclawconfig "jane/pkg/config" + "jane/pkg/runtimepaths" ) const ( - configDirName = ".picoclaw" configFileName = "config.json" ) @@ -22,11 +21,7 @@ func ConfigPath() (string, error) { } func ConfigDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, configDirName), nil + return runtimepaths.HomeDir(), nil } func Load() (*picoclawconfig.Config, error) { diff --git a/cmd/picoclaw/internal/agent/create.go b/cmd/picoclaw/internal/agent/create.go index 4d72a6a1d..155df61da 100644 --- a/cmd/picoclaw/internal/agent/create.go +++ b/cmd/picoclaw/internal/agent/create.go @@ -12,6 +12,7 @@ import ( "jane/cmd/picoclaw/internal" "jane/pkg/config" + "jane/pkg/runtimepaths" ) func NewCreateCommand() *cobra.Command { @@ -65,7 +66,7 @@ func createAgentCmd(name, workspace, sysPrompt, model string, interactive bool) } if workspace == "" { - fmt.Printf("Workspace path (default: ~/.picoclaw/workspace/%s): ", strings.ToLower(strings.ReplaceAll(name, " ", "_"))) + fmt.Printf("Workspace path (default: %s/workspace/%s): ", runtimepaths.HomeDir(), strings.ToLower(strings.ReplaceAll(name, " ", "_"))) workspaceInput, _ := reader.ReadString('\n') workspaceInput = strings.TrimSpace(workspaceInput) if workspaceInput != "" { @@ -95,14 +96,7 @@ func createAgentCmd(name, workspace, sysPrompt, model string, interactive bool) } if workspace == "" { - var homePath string - if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" { - homePath = picoclawHome - } else { - userHome, _ := os.UserHomeDir() - homePath = filepath.Join(userHome, ".picoclaw") - } - workspace = filepath.Join(homePath, "workspace", strings.ToLower(strings.ReplaceAll(name, " ", "_"))) + workspace = filepath.Join(runtimepaths.HomeDir(), "workspace", strings.ToLower(strings.ReplaceAll(name, " ", "_"))) } var modelCfg *config.AgentModelConfig diff --git a/cmd/picoclaw/internal/helpers.go b/cmd/picoclaw/internal/helpers.go index 1ecfbfc08..e348af7c0 100644 --- a/cmd/picoclaw/internal/helpers.go +++ b/cmd/picoclaw/internal/helpers.go @@ -1,10 +1,10 @@ package internal import ( - "os" "path/filepath" "jane/pkg/config" + "jane/pkg/runtimepaths" ) const Logo = "🦞" @@ -12,18 +12,11 @@ const Logo = "🦞" // GetPicoclawHome returns the picoclaw home directory. // Priority: $PICOCLAW_HOME > ~/.picoclaw func GetPicoclawHome() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { - return home - } - home, _ := os.UserHomeDir() - return filepath.Join(home, ".picoclaw") + return runtimepaths.HomeDir() } func GetConfigPath() string { - if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" { - return configPath - } - return filepath.Join(GetPicoclawHome(), "config.json") + return runtimepaths.ConfigPath() } func LoadConfig() (*config.Config, error) { diff --git a/cmd/picoclaw/internal/helpers_test.go b/cmd/picoclaw/internal/helpers_test.go index 583751781..b45ab9d83 100644 --- a/cmd/picoclaw/internal/helpers_test.go +++ b/cmd/picoclaw/internal/helpers_test.go @@ -14,7 +14,7 @@ func TestGetConfigPath(t *testing.T) { t.Setenv("HOME", "/tmp/home") got := GetConfigPath() - want := filepath.Join("/tmp/home", ".picoclaw", "config.json") + want := filepath.Join("/tmp/home", ".jane-ai", "config.json") assert.Equal(t, want, got) } @@ -49,7 +49,7 @@ func TestGetConfigPath_Windows(t *testing.T) { t.Setenv("USERPROFILE", testUserProfilePath) got := GetConfigPath() - want := filepath.Join(testUserProfilePath, ".picoclaw", "config.json") + want := filepath.Join(testUserProfilePath, ".jane-ai", "config.json") require.True(t, strings.EqualFold(got, want), "GetConfigPath() = %q, want %q", got, want) } diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 2bcced2c9..514aacdc0 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -15,6 +15,7 @@ import ( "jane/pkg/config" "jane/pkg/logger" "jane/pkg/providers" + "jane/pkg/runtimepaths" "jane/pkg/skills" "jane/pkg/utils" ) @@ -52,20 +53,13 @@ func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuil } func getGlobalConfigDir() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { - return home - } - home, err := os.UserHomeDir() - if err != nil { - return "" - } - return filepath.Join(home, ".picoclaw") + return runtimepaths.HomeDir() } func NewContextBuilder(workspace string) *ContextBuilder { // builtin skills: skills directory in current project // Use the skills/ directory under the current working directory - builtinSkillsDir := strings.TrimSpace(os.Getenv("PICOCLAW_BUILTIN_SKILLS")) + builtinSkillsDir := strings.TrimSpace(runtimepaths.BuiltinSkillsOverride()) if builtinSkillsDir == "" { wd, _ := os.Getwd() builtinSkillsDir = filepath.Join(wd, "skills") diff --git a/pkg/auth/store.go b/pkg/auth/store.go index b7ebe945a..775fde2f1 100644 --- a/pkg/auth/store.go +++ b/pkg/auth/store.go @@ -7,6 +7,7 @@ import ( "time" "jane/pkg/fileutil" + "jane/pkg/runtimepaths" ) type AuthCredential struct { @@ -39,11 +40,7 @@ func (c *AuthCredential) NeedsRefresh() bool { } func authFilePath() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { - return filepath.Join(home, "auth.json") - } - home, _ := os.UserHomeDir() - return filepath.Join(home, ".picoclaw", "auth.json") + return filepath.Join(runtimepaths.HomeDir(), "auth.json") } func LoadStore() (*AuthStore, error) { diff --git a/pkg/auth/store_test.go b/pkg/auth/store_test.go index f6793cfce..ada83d601 100644 --- a/pkg/auth/store_test.go +++ b/pkg/auth/store_test.go @@ -102,7 +102,7 @@ func TestStoreFilePermissions(t *testing.T) { t.Fatalf("SetCredential() error: %v", err) } - path := filepath.Join(tmpDir, ".picoclaw", "auth.json") + path := filepath.Join(tmpDir, ".jane-ai", "auth.json") info, err := os.Stat(path) if err != nil { t.Fatalf("Stat() error: %v", err) diff --git a/pkg/channels/gmessages/client.go b/pkg/channels/gmessages/client.go index 0d32d7fbc..32db1d214 100644 --- a/pkg/channels/gmessages/client.go +++ b/pkg/channels/gmessages/client.go @@ -11,6 +11,7 @@ import ( "github.com/mdp/qrterminal/v3" "github.com/rs/zerolog" "jane/pkg/logger" + "jane/pkg/runtimepaths" "go.mau.fi/mautrix-gmessages/pkg/libgm" "go.mau.fi/mautrix-gmessages/pkg/libgm/events" @@ -30,11 +31,7 @@ type GMClient struct { func (c *GMessagesChannel) initClient(ctx context.Context) error { dataDir := c.cfg.DataDir if dataDir == "" { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("could not get home dir and data_dir is empty: %w", err) - } - dataDir = filepath.Join(home, ".picoclaw", "gmessages") + dataDir = filepath.Join(runtimepaths.HomeDir(), "gmessages") } if err := os.MkdirAll(dataDir, 0755); err != nil { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 1e510921c..222c752ab 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -6,21 +6,16 @@ package config import ( - "os" "path/filepath" + + "jane/pkg/runtimepaths" ) // DefaultConfig returns the default configuration for PicoClaw. func DefaultConfig() *Config { // Determine the base path for the workspace. // Priority: $PICOCLAW_HOME > ~/.picoclaw - var homePath string - if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" { - homePath = picoclawHome - } else { - userHome, _ := os.UserHomeDir() - homePath = filepath.Join(userHome, ".picoclaw") - } + homePath := runtimepaths.HomeDir() workspacePath := filepath.Join(homePath, "workspace") return &Config{ diff --git a/pkg/runtimepaths/paths.go b/pkg/runtimepaths/paths.go new file mode 100644 index 000000000..7fc6eb7b2 --- /dev/null +++ b/pkg/runtimepaths/paths.go @@ -0,0 +1,43 @@ +package runtimepaths + +import ( + "os" + "path/filepath" + "strings" +) + +func firstEnv(keys ...string) string { + for _, key := range keys { + if value := strings.TrimSpace(os.Getenv(key)); value != "" { + return value + } + } + return "" +} + +func HomeDir() string { + if home := firstEnv("JANE_AI_HOME", "PICOCLAW_HOME"); home != "" { + return home + } + userHome, _ := os.UserHomeDir() + preferred := filepath.Join(userHome, ".jane-ai") + if _, err := os.Stat(preferred); err == nil { + return preferred + } + legacy := filepath.Join(userHome, ".picoclaw") + if _, err := os.Stat(legacy); err == nil { + return legacy + } + return preferred +} + +func ConfigPath() string { + if path := firstEnv("JANE_AI_CONFIG", "PICOCLAW_CONFIG"); path != "" { + return path + } + return filepath.Join(HomeDir(), "config.json") +} + +func BuiltinSkillsOverride() string { + return firstEnv("JANE_AI_BUILTIN_SKILLS", "PICOCLAW_BUILTIN_SKILLS") +} diff --git a/pkg/runtimepaths/paths_test.go b/pkg/runtimepaths/paths_test.go new file mode 100644 index 000000000..5303811a5 --- /dev/null +++ b/pkg/runtimepaths/paths_test.go @@ -0,0 +1,38 @@ +package runtimepaths + +import ( + "os" + "path/filepath" + "testing" +) + +func TestHomeDirPrefersJaneAIByDefault(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + if got := HomeDir(); got != filepath.Join(os.Getenv("HOME"), ".jane-ai") { + t.Fatalf("HomeDir() = %q", got) + } +} + +func TestHomeDirFallsBackToLegacyDir(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + legacy := filepath.Join(home, ".picoclaw") + if err := os.MkdirAll(legacy, 0o755); err != nil { + t.Fatal(err) + } + if got := HomeDir(); got != legacy { + t.Fatalf("HomeDir() = %q, want %q", got, legacy) + } +} + +func TestConfigPathHonorsBothEnvNames(t *testing.T) { + t.Setenv("JANE_AI_CONFIG", "/tmp/jane/config.json") + if got := ConfigPath(); got != "/tmp/jane/config.json" { + t.Fatalf("ConfigPath() = %q", got) + } + t.Setenv("JANE_AI_CONFIG", "") + t.Setenv("PICOCLAW_CONFIG", "/tmp/pico/config.json") + if got := ConfigPath(); got != "/tmp/pico/config.json" { + t.Fatalf("ConfigPath() = %q", got) + } +} diff --git a/web/backend/api/session.go b/web/backend/api/session.go index 1064f81bd..c3e7c1525 100644 --- a/web/backend/api/session.go +++ b/web/backend/api/session.go @@ -14,6 +14,7 @@ import ( "jane/pkg/config" "jane/pkg/providers" + "jane/pkg/runtimepaths" ) // registerSessionRoutes binds session list and detail endpoints to the ServeMux. @@ -248,7 +249,7 @@ func truncateRunes(s string, maxLen int) string { } // sessionsDir resolves the path to the gateway's session storage directory. -// It reads the workspace from config, falling back to ~/.picoclaw/workspace. +// It reads the workspace from config, falling back to the resolved app home workspace. func (h *Handler) sessionsDir() (string, error) { cfg, err := config.LoadConfig(h.configPath) if err != nil { @@ -257,8 +258,7 @@ func (h *Handler) sessionsDir() (string, error) { workspace := cfg.Agents.Defaults.Workspace if workspace == "" { - home, _ := os.UserHomeDir() - workspace = filepath.Join(home, ".picoclaw", "workspace") + workspace = filepath.Join(runtimepaths.HomeDir(), "workspace") } // Expand ~ prefix diff --git a/web/backend/api/skills.go b/web/backend/api/skills.go index 2d1efd34b..cef2503e0 100644 --- a/web/backend/api/skills.go +++ b/web/backend/api/skills.go @@ -11,6 +11,7 @@ import ( "strings" "jane/pkg/config" + "jane/pkg/runtimepaths" "jane/pkg/skills" ) @@ -309,18 +310,11 @@ func loadSkillContent(path string) (string, error) { } func globalConfigDir() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { - return home - } - home, err := os.UserHomeDir() - if err != nil { - return "" - } - return filepath.Join(home, ".picoclaw") + return runtimepaths.HomeDir() } func builtinSkillsDir() string { - if path := os.Getenv("PICOCLAW_BUILTIN_SKILLS"); path != "" { + if path := runtimepaths.BuiltinSkillsOverride(); path != "" { return path } wd, err := os.Getwd() diff --git a/web/backend/utils/runtime.go b/web/backend/utils/runtime.go index 4e6c32c56..96cc62c02 100644 --- a/web/backend/utils/runtime.go +++ b/web/backend/utils/runtime.go @@ -7,47 +7,60 @@ import ( "os/exec" "path/filepath" "runtime" + + "jane/pkg/runtimepaths" ) // GetDefaultConfigPath returns the default path to the picoclaw config file. func GetDefaultConfigPath() string { + if configPath := os.Getenv("JANE_AI_CONFIG"); configPath != "" { + return configPath + } if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" { return configPath } - if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" { - return filepath.Join(picoclawHome, "config.json") + if home := os.Getenv("JANE_AI_HOME"); home != "" { + return filepath.Join(home, "config.json") } - home, err := os.UserHomeDir() - if err != nil { - return "config.json" + if home := os.Getenv("PICOCLAW_HOME"); home != "" { + return filepath.Join(home, "config.json") } - return filepath.Join(home, ".picoclaw", "config.json") + return runtimepaths.ConfigPath() } // FindPicoclawBinary locates the picoclaw executable. // Search order: -// 1. PICOCLAW_BINARY environment variable (explicit override) +// 1. JANE_AI_BINARY or PICOCLAW_BINARY environment variable (explicit override) // 2. Same directory as the current executable -// 3. Falls back to "picoclaw" and relies on $PATH +// 3. Falls back to "jane-ai" or "picoclaw" on $PATH func FindPicoclawBinary() string { - binaryName := "picoclaw" - if runtime.GOOS == "windows" { - binaryName = "picoclaw.exe" + if p := os.Getenv("JANE_AI_BINARY"); p != "" { + if info, _ := os.Stat(p); info != nil && !info.IsDir() { + return p + } } - if p := os.Getenv("PICOCLAW_BINARY"); p != "" { if info, _ := os.Stat(p); info != nil && !info.IsDir() { return p } } + binaryNames := []string{"jane-ai", "picoclaw"} + if runtime.GOOS == "windows" { + binaryNames = []string{"jane-ai.exe", "picoclaw.exe"} + } if exe, err := os.Executable(); err == nil { - candidate := filepath.Join(filepath.Dir(exe), binaryName) - if info, err := os.Stat(candidate); err == nil && !info.IsDir() { - return candidate + for _, binaryName := range binaryNames { + candidate := filepath.Join(filepath.Dir(exe), binaryName) + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + return candidate + } } } + if path, err := exec.LookPath(binaryNames[0]); err == nil { + return path + } return "picoclaw" } diff --git a/web/frontend/src/hooks/use-credentials-page.ts b/web/frontend/src/hooks/use-credentials-page.ts index 354065b23..e4627ca0c 100644 --- a/web/frontend/src/hooks/use-credentials-page.ts +++ b/web/frontend/src/hooks/use-credentials-page.ts @@ -145,7 +145,12 @@ export function useCredentialsPage() { const data = event.data as | { type?: string; flowId?: string; status?: string } | undefined - if (!data || data.type !== "picoclaw-oauth-result" || !data.flowId) { + if ( + !data || + (data.type !== "jane-ai-oauth-result" && + data.type !== "picoclaw-oauth-result") || + !data.flowId + ) { return } diff --git a/web/frontend/src/hooks/use-pico-chat.ts b/web/frontend/src/hooks/use-pico-chat.ts index 7e3066177..854470e55 100644 --- a/web/frontend/src/hooks/use-pico-chat.ts +++ b/web/frontend/src/hooks/use-pico-chat.ts @@ -32,20 +32,25 @@ export interface ChatMessage { type ConnectionState = "disconnected" | "connecting" | "connected" | "error" -const LAST_SESSION_STORAGE_KEY = "picoclaw:last-session-id" +const LAST_SESSION_STORAGE_KEY = "jane-ai:last-session-id" +const LEGACY_SESSION_STORAGE_KEY = "picoclaw:last-session-id" function readStoredSessionId(): string { - const value = localStorage.getItem(LAST_SESSION_STORAGE_KEY)?.trim() + const value = + localStorage.getItem(LAST_SESSION_STORAGE_KEY)?.trim() || + localStorage.getItem(LEGACY_SESSION_STORAGE_KEY)?.trim() return value || "" } function writeStoredSessionId(sessionId: string) { if (sessionId) { localStorage.setItem(LAST_SESSION_STORAGE_KEY, sessionId) + localStorage.setItem(LEGACY_SESSION_STORAGE_KEY, sessionId) return } localStorage.removeItem(LAST_SESSION_STORAGE_KEY) + localStorage.removeItem(LEGACY_SESSION_STORAGE_KEY) } function generateSessionId(): string {