refactor(runtime): add jane-ai compatibility paths\n\n- Centralize home and config path resolution with jane-ai-first fallbacks in pkg/runtimepaths\n- Update auth, skills, sessions, launcher config, and default workspace paths to honor JANE_AI_* and legacy PICOCLAW_* env vars\n- Add frontend compatibility for jane-ai session storage and OAuth postMessage identifiers while preserving legacy picoclaw keys\n- Add runtime path tests and update path-based assertions for the new ~/.jane-ai default\n- Tests passed for pkg/runtimepaths and pkg/auth targeted cases; broader go test is blocked by existing missing go.sum entries

This commit is contained in:
Wadah Adlan 2026-03-28 02:25:58 -04:00
parent badcbaeb97
commit dd69cbeb16
16 changed files with 149 additions and 86 deletions

View file

@ -2,14 +2,13 @@ package configstore
import ( import (
"errors" "errors"
"os"
"path/filepath" "path/filepath"
picoclawconfig "jane/pkg/config" picoclawconfig "jane/pkg/config"
"jane/pkg/runtimepaths"
) )
const ( const (
configDirName = ".picoclaw"
configFileName = "config.json" configFileName = "config.json"
) )
@ -22,11 +21,7 @@ func ConfigPath() (string, error) {
} }
func ConfigDir() (string, error) { func ConfigDir() (string, error) {
home, err := os.UserHomeDir() return runtimepaths.HomeDir(), nil
if err != nil {
return "", err
}
return filepath.Join(home, configDirName), nil
} }
func Load() (*picoclawconfig.Config, error) { func Load() (*picoclawconfig.Config, error) {

View file

@ -12,6 +12,7 @@ import (
"jane/cmd/picoclaw/internal" "jane/cmd/picoclaw/internal"
"jane/pkg/config" "jane/pkg/config"
"jane/pkg/runtimepaths"
) )
func NewCreateCommand() *cobra.Command { func NewCreateCommand() *cobra.Command {
@ -65,7 +66,7 @@ func createAgentCmd(name, workspace, sysPrompt, model string, interactive bool)
} }
if workspace == "" { 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, _ := reader.ReadString('\n')
workspaceInput = strings.TrimSpace(workspaceInput) workspaceInput = strings.TrimSpace(workspaceInput)
if workspaceInput != "" { if workspaceInput != "" {
@ -95,14 +96,7 @@ func createAgentCmd(name, workspace, sysPrompt, model string, interactive bool)
} }
if workspace == "" { if workspace == "" {
var homePath string workspace = filepath.Join(runtimepaths.HomeDir(), "workspace", strings.ToLower(strings.ReplaceAll(name, " ", "_")))
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, " ", "_")))
} }
var modelCfg *config.AgentModelConfig var modelCfg *config.AgentModelConfig

View file

@ -1,10 +1,10 @@
package internal package internal
import ( import (
"os"
"path/filepath" "path/filepath"
"jane/pkg/config" "jane/pkg/config"
"jane/pkg/runtimepaths"
) )
const Logo = "🦞" const Logo = "🦞"
@ -12,18 +12,11 @@ const Logo = "🦞"
// GetPicoclawHome returns the picoclaw home directory. // GetPicoclawHome returns the picoclaw home directory.
// Priority: $PICOCLAW_HOME > ~/.picoclaw // Priority: $PICOCLAW_HOME > ~/.picoclaw
func GetPicoclawHome() string { func GetPicoclawHome() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" { return runtimepaths.HomeDir()
return home
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw")
} }
func GetConfigPath() string { func GetConfigPath() string {
if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" { return runtimepaths.ConfigPath()
return configPath
}
return filepath.Join(GetPicoclawHome(), "config.json")
} }
func LoadConfig() (*config.Config, error) { func LoadConfig() (*config.Config, error) {

View file

@ -14,7 +14,7 @@ func TestGetConfigPath(t *testing.T) {
t.Setenv("HOME", "/tmp/home") t.Setenv("HOME", "/tmp/home")
got := GetConfigPath() got := GetConfigPath()
want := filepath.Join("/tmp/home", ".picoclaw", "config.json") want := filepath.Join("/tmp/home", ".jane-ai", "config.json")
assert.Equal(t, want, got) assert.Equal(t, want, got)
} }
@ -49,7 +49,7 @@ func TestGetConfigPath_Windows(t *testing.T) {
t.Setenv("USERPROFILE", testUserProfilePath) t.Setenv("USERPROFILE", testUserProfilePath)
got := GetConfigPath() 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) require.True(t, strings.EqualFold(got, want), "GetConfigPath() = %q, want %q", got, want)
} }

View file

@ -15,6 +15,7 @@ import (
"jane/pkg/config" "jane/pkg/config"
"jane/pkg/logger" "jane/pkg/logger"
"jane/pkg/providers" "jane/pkg/providers"
"jane/pkg/runtimepaths"
"jane/pkg/skills" "jane/pkg/skills"
"jane/pkg/utils" "jane/pkg/utils"
) )
@ -52,20 +53,13 @@ func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuil
} }
func getGlobalConfigDir() string { func getGlobalConfigDir() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" { return runtimepaths.HomeDir()
return home
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".picoclaw")
} }
func NewContextBuilder(workspace string) *ContextBuilder { func NewContextBuilder(workspace string) *ContextBuilder {
// builtin skills: skills directory in current project // builtin skills: skills directory in current project
// Use the skills/ directory under the current working directory // Use the skills/ directory under the current working directory
builtinSkillsDir := strings.TrimSpace(os.Getenv("PICOCLAW_BUILTIN_SKILLS")) builtinSkillsDir := strings.TrimSpace(runtimepaths.BuiltinSkillsOverride())
if builtinSkillsDir == "" { if builtinSkillsDir == "" {
wd, _ := os.Getwd() wd, _ := os.Getwd()
builtinSkillsDir = filepath.Join(wd, "skills") builtinSkillsDir = filepath.Join(wd, "skills")

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"jane/pkg/fileutil" "jane/pkg/fileutil"
"jane/pkg/runtimepaths"
) )
type AuthCredential struct { type AuthCredential struct {
@ -39,11 +40,7 @@ func (c *AuthCredential) NeedsRefresh() bool {
} }
func authFilePath() string { func authFilePath() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" { return filepath.Join(runtimepaths.HomeDir(), "auth.json")
return filepath.Join(home, "auth.json")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "auth.json")
} }
func LoadStore() (*AuthStore, error) { func LoadStore() (*AuthStore, error) {

View file

@ -102,7 +102,7 @@ func TestStoreFilePermissions(t *testing.T) {
t.Fatalf("SetCredential() error: %v", err) 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) info, err := os.Stat(path)
if err != nil { if err != nil {
t.Fatalf("Stat() error: %v", err) t.Fatalf("Stat() error: %v", err)

View file

@ -11,6 +11,7 @@ import (
"github.com/mdp/qrterminal/v3" "github.com/mdp/qrterminal/v3"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"jane/pkg/logger" "jane/pkg/logger"
"jane/pkg/runtimepaths"
"go.mau.fi/mautrix-gmessages/pkg/libgm" "go.mau.fi/mautrix-gmessages/pkg/libgm"
"go.mau.fi/mautrix-gmessages/pkg/libgm/events" "go.mau.fi/mautrix-gmessages/pkg/libgm/events"
@ -30,11 +31,7 @@ type GMClient struct {
func (c *GMessagesChannel) initClient(ctx context.Context) error { func (c *GMessagesChannel) initClient(ctx context.Context) error {
dataDir := c.cfg.DataDir dataDir := c.cfg.DataDir
if dataDir == "" { if dataDir == "" {
home, err := os.UserHomeDir() dataDir = filepath.Join(runtimepaths.HomeDir(), "gmessages")
if err != nil {
return fmt.Errorf("could not get home dir and data_dir is empty: %w", err)
}
dataDir = filepath.Join(home, ".picoclaw", "gmessages")
} }
if err := os.MkdirAll(dataDir, 0755); err != nil { if err := os.MkdirAll(dataDir, 0755); err != nil {

View file

@ -6,21 +6,16 @@
package config package config
import ( import (
"os"
"path/filepath" "path/filepath"
"jane/pkg/runtimepaths"
) )
// DefaultConfig returns the default configuration for PicoClaw. // DefaultConfig returns the default configuration for PicoClaw.
func DefaultConfig() *Config { func DefaultConfig() *Config {
// Determine the base path for the workspace. // Determine the base path for the workspace.
// Priority: $PICOCLAW_HOME > ~/.picoclaw // Priority: $PICOCLAW_HOME > ~/.picoclaw
var homePath string homePath := runtimepaths.HomeDir()
if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" {
homePath = picoclawHome
} else {
userHome, _ := os.UserHomeDir()
homePath = filepath.Join(userHome, ".picoclaw")
}
workspacePath := filepath.Join(homePath, "workspace") workspacePath := filepath.Join(homePath, "workspace")
return &Config{ return &Config{

43
pkg/runtimepaths/paths.go Normal file
View file

@ -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")
}

View file

@ -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)
}
}

View file

@ -14,6 +14,7 @@ import (
"jane/pkg/config" "jane/pkg/config"
"jane/pkg/providers" "jane/pkg/providers"
"jane/pkg/runtimepaths"
) )
// registerSessionRoutes binds session list and detail endpoints to the ServeMux. // 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. // 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) { func (h *Handler) sessionsDir() (string, error) {
cfg, err := config.LoadConfig(h.configPath) cfg, err := config.LoadConfig(h.configPath)
if err != nil { if err != nil {
@ -257,8 +258,7 @@ func (h *Handler) sessionsDir() (string, error) {
workspace := cfg.Agents.Defaults.Workspace workspace := cfg.Agents.Defaults.Workspace
if workspace == "" { if workspace == "" {
home, _ := os.UserHomeDir() workspace = filepath.Join(runtimepaths.HomeDir(), "workspace")
workspace = filepath.Join(home, ".picoclaw", "workspace")
} }
// Expand ~ prefix // Expand ~ prefix

View file

@ -11,6 +11,7 @@ import (
"strings" "strings"
"jane/pkg/config" "jane/pkg/config"
"jane/pkg/runtimepaths"
"jane/pkg/skills" "jane/pkg/skills"
) )
@ -309,18 +310,11 @@ func loadSkillContent(path string) (string, error) {
} }
func globalConfigDir() string { func globalConfigDir() string {
if home := os.Getenv("PICOCLAW_HOME"); home != "" { return runtimepaths.HomeDir()
return home
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".picoclaw")
} }
func builtinSkillsDir() string { func builtinSkillsDir() string {
if path := os.Getenv("PICOCLAW_BUILTIN_SKILLS"); path != "" { if path := runtimepaths.BuiltinSkillsOverride(); path != "" {
return path return path
} }
wd, err := os.Getwd() wd, err := os.Getwd()

View file

@ -7,47 +7,60 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"jane/pkg/runtimepaths"
) )
// GetDefaultConfigPath returns the default path to the picoclaw config file. // GetDefaultConfigPath returns the default path to the picoclaw config file.
func GetDefaultConfigPath() string { func GetDefaultConfigPath() string {
if configPath := os.Getenv("JANE_AI_CONFIG"); configPath != "" {
return configPath
}
if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" { if configPath := os.Getenv("PICOCLAW_CONFIG"); configPath != "" {
return configPath return configPath
} }
if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" { if home := os.Getenv("JANE_AI_HOME"); home != "" {
return filepath.Join(picoclawHome, "config.json") return filepath.Join(home, "config.json")
} }
home, err := os.UserHomeDir() if home := os.Getenv("PICOCLAW_HOME"); home != "" {
if err != nil { return filepath.Join(home, "config.json")
return "config.json"
} }
return filepath.Join(home, ".picoclaw", "config.json") return runtimepaths.ConfigPath()
} }
// FindPicoclawBinary locates the picoclaw executable. // FindPicoclawBinary locates the picoclaw executable.
// Search order: // 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 // 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 { func FindPicoclawBinary() string {
binaryName := "picoclaw" if p := os.Getenv("JANE_AI_BINARY"); p != "" {
if runtime.GOOS == "windows" { if info, _ := os.Stat(p); info != nil && !info.IsDir() {
binaryName = "picoclaw.exe" return p
}
} }
if p := os.Getenv("PICOCLAW_BINARY"); p != "" { if p := os.Getenv("PICOCLAW_BINARY"); p != "" {
if info, _ := os.Stat(p); info != nil && !info.IsDir() { if info, _ := os.Stat(p); info != nil && !info.IsDir() {
return p 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 { if exe, err := os.Executable(); err == nil {
for _, binaryName := range binaryNames {
candidate := filepath.Join(filepath.Dir(exe), binaryName) candidate := filepath.Join(filepath.Dir(exe), binaryName)
if info, err := os.Stat(candidate); err == nil && !info.IsDir() { if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
return candidate return candidate
} }
} }
}
if path, err := exec.LookPath(binaryNames[0]); err == nil {
return path
}
return "picoclaw" return "picoclaw"
} }

View file

@ -145,7 +145,12 @@ export function useCredentialsPage() {
const data = event.data as const data = event.data as
| { type?: string; flowId?: string; status?: string } | { type?: string; flowId?: string; status?: string }
| undefined | 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 return
} }

View file

@ -32,20 +32,25 @@ export interface ChatMessage {
type ConnectionState = "disconnected" | "connecting" | "connected" | "error" 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 { 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 || "" return value || ""
} }
function writeStoredSessionId(sessionId: string) { function writeStoredSessionId(sessionId: string) {
if (sessionId) { if (sessionId) {
localStorage.setItem(LAST_SESSION_STORAGE_KEY, sessionId) localStorage.setItem(LAST_SESSION_STORAGE_KEY, sessionId)
localStorage.setItem(LEGACY_SESSION_STORAGE_KEY, sessionId)
return return
} }
localStorage.removeItem(LAST_SESSION_STORAGE_KEY) localStorage.removeItem(LAST_SESSION_STORAGE_KEY)
localStorage.removeItem(LEGACY_SESSION_STORAGE_KEY)
} }
function generateSessionId(): string { function generateSessionId(): string {