feat(cli): prefer jane-ai runtime naming\n\n- Rename CLI and launcher-facing help text from picoclaw to Jane AI across root, gateway, onboard, status, migrate, and version commands\n- Make launcher startup and process detection prefer the jane-ai binary while keeping picoclaw fallback support\n- Update container entrypoint and backend launcher usage text to default to jane-ai paths and binary names\n- Add runtime binary resolution in pkg/runtimepaths for shared jane-ai/picoclaw fallback behavior\n- Tests passed for pkg/runtimepaths and pkg/auth; targeted search confirmed the edited runtime strings no longer use picoclaw

This commit is contained in:
Wadah Adlan 2026-03-28 02:31:49 -04:00
parent dd69cbeb16
commit da25deeae0
22 changed files with 104 additions and 88 deletions

View file

@ -12,6 +12,7 @@ import (
configstore "jane/cmd/picoclaw-launcher-tui/internal/config" configstore "jane/cmd/picoclaw-launcher-tui/internal/config"
picoclawconfig "jane/pkg/config" picoclawconfig "jane/pkg/config"
"jane/pkg/runtimepaths"
) )
type appState struct { type appState struct {
@ -204,7 +205,7 @@ func refreshMainMenu(menu *Menu, s *appState) {
}, },
{ {
Label: "Start Talk", Label: "Start Talk",
Description: "Open picoclaw agent in terminal", Description: "Open Jane AI agent in terminal",
Action: func() { Action: func() {
s.requestStartTalk() s.requestStartTalk()
}, },
@ -359,7 +360,7 @@ func (s *appState) startTalk() {
return return
} }
s.app.Suspend(func() { s.app.Suspend(func() {
cmd := exec.Command("picoclaw", "agent") cmd := exec.Command(runtimepaths.BinaryPath(), "agent")
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -380,7 +381,7 @@ func (s *appState) startGateway() {
return return
} }
_ = stopGatewayProcess() _ = stopGatewayProcess()
cmd := exec.Command("picoclaw", "gateway") cmd := exec.Command(runtimepaths.BinaryPath(), "gateway")
logFile, err := os.OpenFile(s.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) logFile, err := os.OpenFile(s.logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil { if err != nil {
s.showMessage("Gateway failed", err.Error()) s.showMessage("Gateway failed", err.Error())

View file

@ -8,11 +8,11 @@ import "os/exec"
var execCommand = exec.Command var execCommand = exec.Command
func isGatewayProcessRunning() bool { func isGatewayProcessRunning() bool {
cmd := execCommand("sh", "-c", "pgrep -f 'picoclaw\\s+gateway' >/dev/null 2>&1") cmd := execCommand("sh", "-c", "pgrep -f '(jane-ai|picoclaw)\\s+gateway' >/dev/null 2>&1")
return cmd.Run() == nil return cmd.Run() == nil
} }
func stopGatewayProcess() error { func stopGatewayProcess() error {
cmd := execCommand("sh", "-c", "pkill -f 'picoclaw\\s+gateway' >/dev/null 2>&1") cmd := execCommand("sh", "-c", "pkill -f '(jane-ai|picoclaw)\\s+gateway' >/dev/null 2>&1")
return cmd.Run() return cmd.Run()
} }

View file

@ -8,11 +8,11 @@ import "os/exec"
var execCommand = exec.Command var execCommand = exec.Command
func isGatewayProcessRunning() bool { func isGatewayProcessRunning() bool {
cmd := execCommand("tasklist", "/FI", "IMAGENAME eq picoclaw.exe") cmd := execCommand("cmd", "/C", `tasklist /FI "IMAGENAME eq jane-ai.exe" | findstr /I /C:"jane-ai.exe" >NUL || tasklist /FI "IMAGENAME eq picoclaw.exe" | findstr /I /C:"picoclaw.exe" >NUL`)
return cmd.Run() == nil return cmd.Run() == nil
} }
func stopGatewayProcess() error { func stopGatewayProcess() error {
cmd := execCommand("taskkill", "/F", "/IM", "picoclaw.exe") cmd := execCommand("cmd", "/C", `taskkill /F /IM jane-ai.exe >NUL 2>&1 || taskkill /F /IM picoclaw.exe >NUL 2>&1`)
return cmd.Run() return cmd.Run()
} }

View file

@ -53,10 +53,10 @@ func TestIsGatewayProcessRunning(t *testing.T) {
if got != tt.wantResult { if got != tt.wantResult {
t.Errorf("isGatewayProcessRunning() = %v, want %v", got, tt.wantResult) t.Errorf("isGatewayProcessRunning() = %v, want %v", got, tt.wantResult)
} }
if gotName != "tasklist" { if gotName != "cmd" {
t.Errorf("expected command name tasklist, got %s", gotName) t.Errorf("expected command name cmd, got %s", gotName)
} }
expectedArgs := []string{"/FI", "IMAGENAME eq picoclaw.exe"} expectedArgs := []string{"/C", `tasklist /FI "IMAGENAME eq jane-ai.exe" | findstr /I /C:"jane-ai.exe" >NUL || tasklist /FI "IMAGENAME eq picoclaw.exe" | findstr /I /C:"picoclaw.exe" >NUL`}
if !reflect.DeepEqual(gotArgs, expectedArgs) { if !reflect.DeepEqual(gotArgs, expectedArgs) {
t.Errorf("expected args %v, got %v", expectedArgs, gotArgs) t.Errorf("expected args %v, got %v", expectedArgs, gotArgs)
} }
@ -106,10 +106,10 @@ func TestStopGatewayProcess(t *testing.T) {
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("stopGatewayProcess() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("stopGatewayProcess() error = %v, wantErr %v", err, tt.wantErr)
} }
if gotName != "taskkill" { if gotName != "cmd" {
t.Errorf("expected command name taskkill, got %s", gotName) t.Errorf("expected command name cmd, got %s", gotName)
} }
expectedArgs := []string{"/F", "/IM", "picoclaw.exe"} expectedArgs := []string{"/C", `taskkill /F /IM jane-ai.exe >NUL 2>&1 || taskkill /F /IM picoclaw.exe >NUL 2>&1`}
if !reflect.DeepEqual(gotArgs, expectedArgs) { if !reflect.DeepEqual(gotArgs, expectedArgs) {
t.Errorf("expected args %v, got %v", expectedArgs, gotArgs) t.Errorf("expected args %v, got %v", expectedArgs, gotArgs)
} }

View file

@ -162,7 +162,7 @@ func authLoginGoogleAntigravity() error {
fmt.Println("\n✓ Google Antigravity login successful!") fmt.Println("\n✓ Google Antigravity login successful!")
fmt.Println("Default model set to: gemini-flash") fmt.Println("Default model set to: gemini-flash")
fmt.Println("Try it: picoclaw agent -m \"Hello world\"") fmt.Println("Try it: jane-ai agent -m \"Hello world\"")
return nil return nil
} }
@ -412,7 +412,7 @@ func authStatusCmd() error {
if len(store.Credentials) == 0 { if len(store.Credentials) == 0 {
fmt.Println("No authenticated providers.") fmt.Println("No authenticated providers.")
fmt.Println("Run: picoclaw auth login --provider <name>") fmt.Println("Run: jane-ai auth login --provider <name>")
return nil return nil
} }
@ -460,7 +460,7 @@ func authModelsCmd() error {
cred, err := auth.GetCredential("google-antigravity") cred, err := auth.GetCredential("google-antigravity")
if err != nil || cred == nil { if err != nil || cred == nil {
return fmt.Errorf( return fmt.Errorf(
"not logged in to Google Antigravity.\nrun: picoclaw auth login --provider google-antigravity", "not logged in to Google Antigravity.\nrun: jane-ai auth login --provider google-antigravity",
) )
} }

View file

@ -16,7 +16,7 @@ func NewGatewayCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "gateway", Use: "gateway",
Aliases: []string{"g"}, Aliases: []string{"g"},
Short: "Start picoclaw gateway", Short: "Start the Jane AI gateway",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRunE: func(_ *cobra.Command, _ []string) error { PreRunE: func(_ *cobra.Command, _ []string) error {
if noTruncate && !debug { if noTruncate && !debug {

View file

@ -13,7 +13,7 @@ func TestNewGatewayCommand(t *testing.T) {
require.NotNil(t, cmd) require.NotNil(t, cmd)
assert.Equal(t, "gateway", cmd.Use) assert.Equal(t, "gateway", cmd.Use)
assert.Equal(t, "Start picoclaw gateway", cmd.Short) assert.Equal(t, "Start the Jane AI gateway", cmd.Short)
assert.Len(t, cmd.Aliases, 1) assert.Len(t, cmd.Aliases, 1)
assert.True(t, cmd.HasAlias("g")) assert.True(t, cmd.HasAlias("g"))

View file

@ -11,13 +11,13 @@ func NewMigrateCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "migrate", Use: "migrate",
Short: "Migrate from xxxclaw(openclaw, etc.) to picoclaw", Short: "Migrate from xxxclaw(openclaw, etc.) to Jane AI",
Args: cobra.NoArgs, Args: cobra.NoArgs,
Example: ` picoclaw migrate Example: ` jane-ai migrate
picoclaw migrate --from openclaw jane-ai migrate --from openclaw
picoclaw migrate --dry-run jane-ai migrate --dry-run
picoclaw migrate --refresh jane-ai migrate --refresh
picoclaw migrate --force`, jane-ai migrate --force`,
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
m := migrate.NewMigrateInstance(opts) m := migrate.NewMigrateInstance(opts)
result, err := m.Run(opts) result, err := m.Run(opts)

View file

@ -13,7 +13,7 @@ func TestNewMigrateCommand(t *testing.T) {
require.NotNil(t, cmd) require.NotNil(t, cmd)
assert.Equal(t, "migrate", cmd.Use) assert.Equal(t, "migrate", cmd.Use)
assert.Equal(t, "Migrate from xxxclaw(openclaw, etc.) to picoclaw", cmd.Short) assert.Equal(t, "Migrate from xxxclaw(openclaw, etc.) to Jane AI", cmd.Short)
assert.Len(t, cmd.Aliases, 0) assert.Len(t, cmd.Aliases, 0)

View file

@ -14,7 +14,7 @@ func NewOnboardCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "onboard", Use: "onboard",
Aliases: []string{"o"}, Aliases: []string{"o"},
Short: "Initialize picoclaw configuration and workspace", Short: "Initialize Jane AI configuration and workspace",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
onboard() onboard()
}, },

View file

@ -13,7 +13,7 @@ func TestNewOnboardCommand(t *testing.T) {
require.NotNil(t, cmd) require.NotNil(t, cmd)
assert.Equal(t, "onboard", cmd.Use) assert.Equal(t, "onboard", cmd.Use)
assert.Equal(t, "Initialize picoclaw configuration and workspace", cmd.Short) assert.Equal(t, "Initialize Jane AI configuration and workspace", cmd.Short)
assert.Len(t, cmd.Aliases, 1) assert.Len(t, cmd.Aliases, 1)
assert.True(t, cmd.HasAlias("o")) assert.True(t, cmd.HasAlias("o"))

View file

@ -33,7 +33,7 @@ func onboard() {
workspace := cfg.WorkspacePath() workspace := cfg.WorkspacePath()
createWorkspaceTemplates(workspace) createWorkspaceTemplates(workspace)
fmt.Printf("%s picoclaw is ready!\n", internal.Logo) fmt.Printf("%s Jane AI is ready!\n", internal.Logo)
fmt.Println("\nNext steps:") fmt.Println("\nNext steps:")
fmt.Println(" 1. Add your API key to", configPath) fmt.Println(" 1. Add your API key to", configPath)
fmt.Println("") fmt.Println("")
@ -43,7 +43,7 @@ func onboard() {
fmt.Println("") fmt.Println("")
fmt.Println(" See README.md for 17+ supported providers.") fmt.Println(" See README.md for 17+ supported providers.")
fmt.Println("") fmt.Println("")
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"") fmt.Println(" 2. Chat: jane-ai agent -m \"Hello!\"")
} }
func createWorkspaceTemplates(workspace string) { func createWorkspaceTemplates(workspace string) {

View file

@ -8,7 +8,7 @@ func NewStatusCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "status", Use: "status",
Aliases: []string{"s"}, Aliases: []string{"s"},
Short: "Show picoclaw status", Short: "Show Jane AI status",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
statusCmd() statusCmd()
}, },

View file

@ -17,7 +17,7 @@ func TestNewStatusCommand(t *testing.T) {
assert.Len(t, cmd.Aliases, 1) assert.Len(t, cmd.Aliases, 1)
assert.True(t, cmd.HasAlias("s")) assert.True(t, cmd.HasAlias("s"))
assert.Equal(t, "Show picoclaw status", cmd.Short) assert.Equal(t, "Show Jane AI status", cmd.Short)
assert.False(t, cmd.HasSubCommands()) assert.False(t, cmd.HasSubCommands())

View file

@ -18,7 +18,7 @@ func statusCmd() {
configPath := internal.GetConfigPath() configPath := internal.GetConfigPath()
fmt.Printf("%s picoclaw Status\n", internal.Logo) fmt.Printf("%s Jane AI Status\n", internal.Logo)
fmt.Printf("Version: %s\n", config.FormatVersion()) fmt.Printf("Version: %s\n", config.FormatVersion())
build, _ := config.FormatBuildInfo() build, _ := config.FormatBuildInfo()
if build != "" { if build != "" {

View file

@ -23,7 +23,7 @@ func NewVersionCommand() *cobra.Command {
} }
func printVersion() { func printVersion() {
fmt.Printf("%s picoclaw %s\n", internal.Logo, config.FormatVersion()) fmt.Printf("%s jane-ai %s\n", internal.Logo, config.FormatVersion())
build, goVer := config.FormatBuildInfo() build, goVer := config.FormatBuildInfo()
if build != "" { if build != "" {
fmt.Printf(" Build: %s\n", build) fmt.Printf(" Build: %s\n", build)

View file

@ -27,12 +27,12 @@ import (
) )
func NewPicoclawCommand() *cobra.Command { func NewPicoclawCommand() *cobra.Command {
short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, config.GetVersion()) short := fmt.Sprintf("%s jane-ai - Personal AI Assistant v%s\n\n", internal.Logo, config.GetVersion())
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "picoclaw", Use: "jane-ai",
Short: short, Short: short,
Example: "picoclaw version", Example: "jane-ai version",
} }
cmd.AddCommand( cmd.AddCommand(
@ -52,16 +52,16 @@ func NewPicoclawCommand() *cobra.Command {
} }
const ( const (
colorBlue = "\033[1;38;2;62;93;185m" colorBlue = "\033[1;38;2;62;93;185m"
colorRed = "\033[1;38;2;213;70;70m" colorRed = "\033[1;38;2;213;70;70m"
banner = "\r\n" + banner = "\r\n" +
colorBlue + "██████╗ █████╗ ███╗ ██╗███████╗" + colorRed + " █████╗ ██╗\n" + colorBlue + "██████╗ █████╗ ███╗ ██╗███████╗" + colorRed + " █████╗ ██╗\n" +
colorBlue + "╚══██╔╝██╔══██╗████╗ ██║██╔════╝" + colorRed + " ██╔══██╗██║\n" + colorBlue + "╚══██╔╝██╔══██╗████╗ ██║██╔════╝" + colorRed + " ██╔══██╗██║\n" +
colorBlue + " ██║ ███████║██╔██╗ ██║█████╗ " + colorRed + " ███████║██║\n" + colorBlue + " ██║ ███████║██╔██╗ ██║█████╗ " + colorRed + " ███████║██║\n" +
colorBlue + " ██║ ██╔══██║██║╚██╗██║██╔══╝ " + colorRed + " ██╔══██║██║\n" + colorBlue + " ██║ ██╔══██║██║╚██╗██║██╔══╝ " + colorRed + " ██╔══██║██║\n" +
colorBlue + "█████║ ██║ ██║██║ ╚████║███████╗" + colorRed + " ██╗██║ ██║██║\n" + colorBlue + "█████║ ██║ ██║██║ ╚████║███████╗" + colorRed + " ██╗██║ ██║██║\n" +
colorBlue + "╚════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝" + colorRed + " ╚═╝╚═╝ ╚═╝╚═╝\n" + colorBlue + "╚════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝" + colorRed + " ╚═╝╚═╝ ╚═╝╚═╝\n" +
"\033[0m\r\n" "\033[0m\r\n"
) )
func main() { func main() {

View file

@ -4,12 +4,22 @@ set -e
# First-run: neither config nor workspace exists. # First-run: neither config nor workspace exists.
# If config.json is already mounted but workspace is missing we skip onboard to # If config.json is already mounted but workspace is missing we skip onboard to
# avoid the interactive "Overwrite? (y/n)" prompt hanging in a non-TTY container. # avoid the interactive "Overwrite? (y/n)" prompt hanging in a non-TTY container.
if [ ! -d "${HOME}/.picoclaw/workspace" ] && [ ! -f "${HOME}/.picoclaw/config.json" ]; then BIN="${JANE_AI_BINARY:-jane-ai}"
picoclaw onboard if ! command -v "$BIN" >/dev/null 2>&1; then
BIN="picoclaw"
fi
HOME_DIR="${JANE_AI_HOME:-${PICOCLAW_HOME:-${HOME}/.jane-ai}}"
if [ ! -e "${HOME_DIR}/config.json" ] && [ -e "${HOME}/.picoclaw/config.json" ]; then
HOME_DIR="${HOME}/.picoclaw"
fi
if [ ! -d "${HOME_DIR}/workspace" ] && [ ! -f "${HOME_DIR}/config.json" ]; then
"$BIN" onboard
echo "" echo ""
echo "First-run setup complete." echo "First-run setup complete."
echo "Edit ${HOME}/.picoclaw/config.json (add your API key, etc.) then restart the container." echo "Edit ${HOME_DIR}/config.json (add your API key, etc.) then restart the container."
exit 0 exit 0
fi fi
exec picoclaw gateway "$@" exec "$BIN" gateway "$@"

View file

@ -79,9 +79,9 @@ func (cb *ContextBuilder) getIdentity() string {
version := config.FormatVersion() version := config.FormatVersion()
return fmt.Sprintf( return fmt.Sprintf(
`# picoclaw 🦞 (%s) `# Jane AI (%s)
You are picoclaw, a helpful AI assistant. You are Jane AI, a helpful AI assistant.
## Workspace ## Workspace
Your workspace is at: %s Your workspace is at: %s

View file

@ -0,0 +1,32 @@
package runtimepaths
import (
"os"
"os/exec"
"path/filepath"
"runtime"
)
func BinaryPath() string {
if path := firstEnv("JANE_AI_BINARY", "PICOCLAW_BINARY"); path != "" {
return path
}
names := []string{"jane-ai", "picoclaw"}
if runtime.GOOS == "windows" {
names = []string{"jane-ai.exe", "picoclaw.exe"}
}
if exe, err := os.Executable(); err == nil {
for _, name := range names {
path := filepath.Join(filepath.Dir(exe), name)
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return path
}
}
}
for _, name := range names {
if path, err := exec.LookPath(name); err == nil {
return path
}
}
return names[len(names)-1]
}

View file

@ -1,13 +1,13 @@
// PicoClaw Web Console - Web-based chat and management interface // Jane AI Web Console - Web-based chat and management interface
// //
// Provides a web UI for chatting with PicoClaw via the Pico Channel WebSocket, // Provides a web UI for chatting with Jane AI via the Pico Channel WebSocket,
// with configuration management and gateway process control. // with configuration management and gateway process control.
// //
// Usage: // Usage:
// //
// go build -o picoclaw-web ./web/backend/ // go build -o jane-ai-web ./web/backend/
// ./picoclaw-web [config.json] // ./jane-ai-web [config.json]
// ./picoclaw-web -public config.json // ./jane-ai-web -public config.json
package main package main
@ -34,10 +34,10 @@ func main() {
noBrowser := flag.Bool("no-browser", false, "Do not auto-open browser on startup") noBrowser := flag.Bool("no-browser", false, "Do not auto-open browser on startup")
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "PicoClaw Launcher - A web-based configuration editor\n\n") fmt.Fprintf(os.Stderr, "Jane AI Launcher - A web-based configuration editor\n\n")
fmt.Fprintf(os.Stderr, "Usage: %s [options] [config.json]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Usage: %s [options] [config.json]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Arguments:\n") fmt.Fprintf(os.Stderr, "Arguments:\n")
fmt.Fprintf(os.Stderr, " config.json Path to the configuration file (default: ~/.picoclaw/config.json)\n\n") fmt.Fprintf(os.Stderr, " config.json Path to the configuration file (default: ~/.jane-ai/config.json)\n\n")
fmt.Fprintf(os.Stderr, "Options:\n") fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults() flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, "\nExamples:\n")
@ -63,7 +63,7 @@ func main() {
} }
err = utils.EnsureOnboarded(absPath) err = utils.EnsureOnboarded(absPath)
if err != nil { if err != nil {
logger.WarnCF("main", "Failed to initialize PicoClaw config automatically", map[string]any{"error": err.Error()}) logger.WarnCF("main", "Failed to initialize Jane AI config automatically", map[string]any{"error": err.Error()})
} }
var explicitPort bool var explicitPort bool

View file

@ -34,34 +34,7 @@ func GetDefaultConfigPath() string {
// 2. Same directory as the current executable // 2. Same directory as the current executable
// 3. Falls back to "jane-ai" or "picoclaw" on $PATH // 3. Falls back to "jane-ai" or "picoclaw" on $PATH
func FindPicoclawBinary() string { func FindPicoclawBinary() string {
if p := os.Getenv("JANE_AI_BINARY"); p != "" { return runtimepaths.BinaryPath()
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 {
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"
} }
// GetLocalIP returns the local IP address of the machine. // GetLocalIP returns the local IP address of the machine.