diff --git a/cmd/picoclaw/internal/helpers.go b/cmd/picoclaw/internal/helpers.go index e04bccffb..6b2d65c91 100644 --- a/cmd/picoclaw/internal/helpers.go +++ b/cmd/picoclaw/internal/helpers.go @@ -12,7 +12,7 @@ const Logo = "🦞" // GetPicoclawHome returns the picoclaw home directory. // Priority: $PICOCLAW_HOME > ~/.picoclaw func GetPicoclawHome() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { + if home := os.Getenv(config.EnvHome); home != "" { return home } home, _ := os.UserHomeDir() @@ -20,7 +20,7 @@ func GetPicoclawHome() string { } func GetConfigPath() string { - if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" { + if configPath := os.Getenv(config.EnvConfig); configPath != "" { return configPath } return filepath.Join(GetPicoclawHome(), "config.json") diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 830edf875..8db8f0b5e 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -52,7 +52,7 @@ func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuil } func getGlobalConfigDir() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { + if home := os.Getenv(config.EnvHome); home != "" { return home } home, err := os.UserHomeDir() @@ -65,7 +65,7 @@ func getGlobalConfigDir() string { 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(os.Getenv(config.EnvBuiltinSkills)) if builtinSkillsDir == "" { wd, _ := os.Getwd() builtinSkillsDir = filepath.Join(wd, "skills") diff --git a/pkg/auth/store.go b/pkg/auth/store.go index 2e55d4877..f7813ca57 100644 --- a/pkg/auth/store.go +++ b/pkg/auth/store.go @@ -6,6 +6,7 @@ import ( "path/filepath" "time" + "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/fileutil" ) @@ -39,7 +40,7 @@ func (c *AuthCredential) NeedsRefresh() bool { } func authFilePath() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { + if home := os.Getenv(config.EnvHome); home != "" { return filepath.Join(home, "auth.json") } home, _ := os.UserHomeDir() diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 9e8668779..eca8af1bf 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -15,7 +15,7 @@ func DefaultConfig() *Config { // Determine the base path for the workspace. // Priority: $PICOCLAW_HOME > ~/.picoclaw var homePath string - if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" { + if picoclawHome := os.Getenv(EnvHome); picoclawHome != "" { homePath = picoclawHome } else { userHome, _ := os.UserHomeDir() diff --git a/pkg/config/envkeys.go b/pkg/config/envkeys.go new file mode 100644 index 000000000..b04ff19f5 --- /dev/null +++ b/pkg/config/envkeys.go @@ -0,0 +1,37 @@ +// PicoClaw - Ultra-lightweight personal AI agent +// License: MIT +// +// Copyright (c) 2026 PicoClaw contributors + +package config + +// Runtime environment variable keys for the picoclaw process. +// These control the location of files and binaries at runtime and are read +// directly via os.Getenv / os.LookupEnv. All picoclaw-specific keys use the +// PICOCLAW_ prefix. Reference these constants instead of inline string +// literals to keep all supported knobs visible in one place and to prevent +// typos. +const ( + // EnvHome overrides the base directory for all picoclaw data + // (config, workspace, skills, auth store, …). + // Default: ~/.picoclaw + EnvHome = "PICOCLAW_HOME" + + // EnvConfig overrides the full path to the JSON config file. + // Default: $PICOCLAW_HOME/config.json + EnvConfig = "PICOCLAW_CONFIG" + + // EnvBuiltinSkills overrides the directory from which built-in + // skills are loaded. + // Default: /skills + EnvBuiltinSkills = "PICOCLAW_BUILTIN_SKILLS" + + // EnvBinary overrides the path to the picoclaw executable. + // Used by the web launcher when spawning the gateway subprocess. + // Default: resolved from the same directory as the current executable. + EnvBinary = "PICOCLAW_BINARY" + + // EnvGatewayHost overrides the host address for the gateway server. + // Default: "127.0.0.1" + EnvGatewayHost = "PICOCLAW_GATEWAY_HOST" +) diff --git a/pkg/credential/credential.go b/pkg/credential/credential.go index 83af3fc9f..b65c19446 100644 --- a/pkg/credential/credential.go +++ b/pkg/credential/credential.go @@ -66,6 +66,14 @@ var ErrPassphraseRequired = errors.New("credential: enc:// passphrase required") // indicating a wrong passphrase or SSH key. Callers can detect this with errors.Is. var ErrDecryptionFailed = errors.New("credential: enc:// decryption failed (wrong passphrase or SSH key?)") +// SSHKeyPathEnvVar is the environment variable that specifies the path to the +// SSH private key used for enc:// credential encryption and decryption. +const SSHKeyPathEnvVar = "PICOCLAW_SSH_KEY_PATH" + +// picoclawHome is a package-local copy of config.EnvHome. It is kept here to +// avoid a circular import between pkg/credential and pkg/config. +const picoclawHome = "PICOCLAW_HOME" + const ( fileScheme = "file://" encScheme = "enc://" @@ -73,7 +81,6 @@ const ( saltLen = 16 nonceLen = 12 keyLen = 32 - sshKeyEnv = "PICOCLAW_SSH_KEY_PATH" ) // Resolver resolves raw credential strings for model_list api_key fields. @@ -248,14 +255,14 @@ func allowedSSHKeyPath(path string) bool { clean := filepath.Clean(path) // Exact match with PICOCLAW_SSH_KEY_PATH. - if envPath, ok := os.LookupEnv(sshKeyEnv); ok && envPath != "" { + if envPath, ok := os.LookupEnv(SSHKeyPathEnvVar); ok && envPath != "" { if clean == filepath.Clean(envPath) { return true } } // Within PICOCLAW_HOME. - if picoHome := os.Getenv("PICOCLAW_HOME"); picoHome != "" { + if picoHome := os.Getenv(picoclawHome); picoHome != "" { if isWithinDir(clean, picoHome) { return true } @@ -316,7 +323,7 @@ func pickSSHKeyPath(override string) string { if override != "" { return override } - if p, ok := os.LookupEnv(sshKeyEnv); ok { + if p, ok := os.LookupEnv(SSHKeyPathEnvVar); ok { return p // respect explicit setting, even if "" } return findDefaultSSHKey() diff --git a/pkg/migrate/internal/common.go b/pkg/migrate/internal/common.go index c77ab9f26..75aef5dc2 100644 --- a/pkg/migrate/internal/common.go +++ b/pkg/migrate/internal/common.go @@ -5,13 +5,15 @@ import ( "io" "os" "path/filepath" + + "github.com/sipeed/picoclaw/pkg/config" ) func ResolveTargetHome(override string) (string, error) { if override != "" { return ExpandHome(override), nil } - if envHome := os.Getenv("PICOCLAW_HOME"); envHome != "" { + if envHome := os.Getenv(config.EnvHome); envHome != "" { return ExpandHome(envHome), nil } home, err := os.UserHomeDir() diff --git a/pkg/migrate/sources/openclaw/openclaw_handler.go b/pkg/migrate/sources/openclaw/openclaw_handler.go index aaff119f1..5e5241268 100644 --- a/pkg/migrate/sources/openclaw/openclaw_handler.go +++ b/pkg/migrate/sources/openclaw/openclaw_handler.go @@ -10,6 +10,11 @@ import ( "github.com/sipeed/picoclaw/pkg/migrate/internal" ) +// OpenclawHomeEnvVar is the environment variable that overrides the source +// openclaw home directory when migrating from openclaw to picoclaw. +// Default: ~/.openclaw +const OpenclawHomeEnvVar = "OPENCLAW_HOME" + var providerMapping = map[string]string{ "anthropic": "anthropic", "claude": "anthropic", @@ -112,7 +117,7 @@ func resolveSourceHome(override string) (string, error) { if override != "" { return internal.ExpandHome(override), nil } - if envHome := os.Getenv("OPENCLAW_HOME"); envHome != "" { + if envHome := os.Getenv(OpenclawHomeEnvVar); envHome != "" { return internal.ExpandHome(envHome), nil } home, err := os.UserHomeDir() diff --git a/pkg/providers/codex_cli_credentials.go b/pkg/providers/codex_cli_credentials.go index 40f3ee2a1..c5b25f040 100644 --- a/pkg/providers/codex_cli_credentials.go +++ b/pkg/providers/codex_cli_credentials.go @@ -8,6 +8,11 @@ import ( "time" ) +// CodexHomeEnvVar is the environment variable that overrides the Codex CLI +// home directory when resolving the codex auth.json credentials file. +// Default: ~/.codex +const CodexHomeEnvVar = "CODEX_HOME" + // CodexCliAuth represents the ~/.codex/auth.json file structure. type CodexCliAuth struct { Tokens struct { @@ -69,7 +74,7 @@ func CreateCodexCliTokenSource() func() (string, string, error) { } func resolveCodexAuthPath() (string, error) { - codexHome := os.Getenv("CODEX_HOME") + codexHome := os.Getenv(CodexHomeEnvVar) if codexHome == "" { home, err := os.UserHomeDir() if err != nil { diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go index 098e2babe..f454630aa 100644 --- a/web/backend/api/gateway.go +++ b/web/backend/api/gateway.go @@ -387,10 +387,10 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int // GetConfigPath() already reads, so the gateway sub-process uses the same // config file without requiring a --config flag on the gateway subcommand. if h.configPath != "" { - cmd.Env = append(cmd.Env, "PICOCLAW_CONFIG="+h.configPath) + cmd.Env = append(cmd.Env, config.EnvConfig+"="+h.configPath) } if host := h.gatewayHostOverride(); host != "" { - cmd.Env = append(cmd.Env, "PICOCLAW_GATEWAY_HOST="+host) + cmd.Env = append(cmd.Env, config.EnvGatewayHost+"="+host) } stdoutPipe, err := cmd.StdoutPipe() diff --git a/web/backend/api/skills.go b/web/backend/api/skills.go index 936074fee..3c2fb57dd 100644 --- a/web/backend/api/skills.go +++ b/web/backend/api/skills.go @@ -309,7 +309,7 @@ func loadSkillContent(path string) (string, error) { } func globalConfigDir() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { + if home := os.Getenv(config.EnvHome); home != "" { return home } home, err := os.UserHomeDir() @@ -320,7 +320,7 @@ func globalConfigDir() string { } func builtinSkillsDir() string { - if path := os.Getenv("PICOCLAW_BUILTIN_SKILLS"); path != "" { + if path := os.Getenv(config.EnvBuiltinSkills); path != "" { return path } wd, err := os.Getwd() diff --git a/web/backend/utils/onboard.go b/web/backend/utils/onboard.go index fbe34f220..81475ac80 100644 --- a/web/backend/utils/onboard.go +++ b/web/backend/utils/onboard.go @@ -5,6 +5,8 @@ import ( "os" "os/exec" "strings" + + "github.com/sipeed/picoclaw/pkg/config" ) var execCommand = exec.Command @@ -19,7 +21,7 @@ func EnsureOnboarded(configPath string) error { } cmd := execCommand(FindPicoclawBinary(), "onboard") - cmd.Env = append(os.Environ(), "PICOCLAW_CONFIG="+configPath) + cmd.Env = append(os.Environ(), config.EnvConfig+"="+configPath) cmd.Stdin = strings.NewReader("n\n") output, err := cmd.CombinedOutput() diff --git a/web/backend/utils/runtime.go b/web/backend/utils/runtime.go index 425f25c08..772cd7ec0 100644 --- a/web/backend/utils/runtime.go +++ b/web/backend/utils/runtime.go @@ -7,20 +7,23 @@ import ( "os/exec" "path/filepath" "runtime" + + "github.com/sipeed/picoclaw/pkg/config" ) // GetPicoclawHome returns the picoclaw home directory. // Priority: $PICOCLAW_HOME > ~/.picoclaw func GetPicoclawHome() string { - if home := os.Getenv("PICOCLAW_HOME"); home != "" { + if home := os.Getenv(config.EnvHome); home != "" { return home } home, _ := os.UserHomeDir() return filepath.Join(home, ".picoclaw") } +// GetDefaultConfigPath returns the default path to the picoclaw config file. func GetDefaultConfigPath() string { - if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" { + if configPath := os.Getenv(config.EnvConfig); configPath != "" { return configPath } return filepath.Join(GetPicoclawHome(), "config.json") @@ -37,7 +40,7 @@ func FindPicoclawBinary() string { binaryName = "picoclaw.exe" } - if p := os.Getenv("PICOCLAW_BINARY"); p != "" { + if p := os.Getenv(config.EnvBinary); p != "" { if info, _ := os.Stat(p); info != nil && !info.IsDir() { return p }