refactor main.go with cobra

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
This commit is contained in:
Kai Xia 2026-02-15 23:59:40 +11:00
parent 56a060ff61
commit 2b9b7720e3
30 changed files with 1935 additions and 1421 deletions

175
cmd/picoclaw/agent.go Normal file
View file

@ -0,0 +1,175 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/chzyer/readline"
"github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/spf13/cobra"
)
var agentCmd = &cobra.Command{
Use: "agent",
Short: "Interact with the agent directly",
Long: `Interact with the AI agent directly via command line or interactive mode.`,
Run: func(cmd *cobra.Command, args []string) {
agentImpl()
},
}
var (
agentDebug bool
agentMessage string
agentSession string
)
func init() {
agentCmd.Flags().BoolVarP(&agentDebug, "debug", "d", false, "Enable debug mode")
agentCmd.Flags().StringVarP(&agentMessage, "message", "m", "", "Send a single message to the agent")
agentCmd.Flags().StringVarP(&agentSession, "session", "s", "cli:default", "Session key for conversation")
}
func agentImpl() {
message := agentMessage
sessionKey := agentSession
if agentDebug {
logger.SetLevel(logger.DEBUG)
fmt.Println("🔍 Debug mode enabled")
}
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
provider, err := providers.CreateProvider(cfg)
if err != nil {
fmt.Printf("Error creating provider: %v\n", err)
os.Exit(1)
}
msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
// Print agent startup info (only for interactive mode)
startupInfo := agentLoop.GetStartupInfo()
logger.InfoCF("agent", "Agent initialized",
map[string]interface{}{
"tools_count": startupInfo["tools"].(map[string]interface{})["count"],
"skills_total": startupInfo["skills"].(map[string]interface{})["total"],
"skills_available": startupInfo["skills"].(map[string]interface{})["available"],
})
if message != "" {
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("\n%s %s\n", logo, response)
} else {
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
interactiveMode(agentLoop, sessionKey)
}
}
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
prompt := fmt.Sprintf("%s You: ", logo)
rl, err := readline.NewEx(&readline.Config{
Prompt: prompt,
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
HistoryLimit: 100,
InterruptPrompt: "^C",
EOFPrompt: "exit",
})
if err != nil {
fmt.Printf("Error initializing readline: %v\n", err)
fmt.Println("Falling back to simple input mode...")
simpleInteractiveMode(agentLoop, sessionKey)
return
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil {
if err == readline.ErrInterrupt || err == io.EOF {
fmt.Println("\nGoodbye!")
return
}
fmt.Printf("Error reading input: %v\n", err)
continue
}
input := strings.TrimSpace(line)
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("Goodbye!")
return
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", logo, response)
}
}
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print(fmt.Sprintf("%s You: ", logo))
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Println("\nGoodbye!")
return
}
fmt.Printf("Error reading input: %v\n", err)
continue
}
input := strings.TrimSpace(line)
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("Goodbye!")
return
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", logo, response)
}
}

44
cmd/picoclaw/auth.go Normal file
View file

@ -0,0 +1,44 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"github.com/sipeed/picoclaw/cmd/picoclaw/auth"
"github.com/spf13/cobra"
)
var authCmd = &cobra.Command{
Use: "auth",
Short: "Manage authentication",
Long: `Manage authentication for different providers (login, logout, status).`,
Run: func(cmd *cobra.Command, args []string) {
authHelp()
},
}
func init() {
authCmd.AddCommand(auth.LoginCmd)
authCmd.AddCommand(auth.LogoutCmd)
authCmd.AddCommand(auth.StatusCmd)
}
func authHelp() {
fmt.Println("\nAuth commands:")
fmt.Println(" login Login via OAuth or paste token")
fmt.Println(" logout Remove stored credentials")
fmt.Println(" status Show current auth status")
fmt.Println()
fmt.Println("Login options:")
fmt.Println(" --provider <name> Provider to login with (openai, anthropic)")
fmt.Println(" --device-code Use device code flow (for headless environments)")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" picoclaw auth login --provider openai")
fmt.Println(" picoclaw auth login --provider openai --device-code")
fmt.Println(" picoclaw auth login --provider anthropic")
fmt.Println(" picoclaw auth logout --provider openai")
fmt.Println(" picoclaw auth status")
}

124
cmd/picoclaw/auth/login.go Normal file
View file

@ -0,0 +1,124 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package auth
import (
"fmt"
"os"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/spf13/cobra"
)
var LoginCmd = &cobra.Command{
Use: "login",
Short: "Login via OAuth or paste token",
Long: `Login to a provider using OAuth browser flow or paste a token.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if loginProvider == "" {
return fmt.Errorf("--provider is required")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
loginImpl()
},
}
var (
loginProvider string
loginDeviceCode bool
)
func init() {
LoginCmd.Flags().StringVarP(&loginProvider, "provider", "p", "", "Provider to login with (openai, anthropic)")
LoginCmd.Flags().BoolVar(&loginDeviceCode, "device-code", false, "Use device code flow (for headless environments)")
}
func getConfigPath() string {
home, _ := os.UserHomeDir()
return fmt.Sprintf("%s/.picoclaw/config.json", home)
}
func loadConfig() (*config.Config, error) {
return config.LoadConfig(getConfigPath())
}
func loginImpl() {
switch loginProvider {
case "openai":
loginOpenAI(loginDeviceCode)
case "anthropic":
loginPasteToken(loginProvider)
default:
fmt.Printf("Unsupported provider: %s\n", loginProvider)
fmt.Println("Supported providers: openai, anthropic")
os.Exit(1)
}
}
func loginOpenAI(useDeviceCode bool) {
cfg := auth.OpenAIOAuthConfig()
var cred *auth.AuthCredential
var err error
if useDeviceCode {
cred, err = auth.LoginDeviceCode(cfg)
} else {
cred, err = auth.LoginBrowser(cfg)
}
if err != nil {
fmt.Printf("Login failed: %v\n", err)
os.Exit(1)
}
if err := auth.SetCredential("openai", cred); err != nil {
fmt.Printf("Failed to save credentials: %v\n", err)
os.Exit(1)
}
appCfg, err := loadConfig()
if err == nil {
appCfg.Providers.OpenAI.AuthMethod = "oauth"
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
fmt.Printf("Warning: could not update config: %v\n", err)
}
}
fmt.Println("Login successful!")
if cred.AccountID != "" {
fmt.Printf("Account: %s\n", cred.AccountID)
}
}
func loginPasteToken(provider string) {
cred, err := auth.LoginPasteToken(provider, os.Stdin)
if err != nil {
fmt.Printf("Login failed: %v\n", err)
os.Exit(1)
}
if err := auth.SetCredential(provider, cred); err != nil {
fmt.Printf("Failed to save credentials: %v\n", err)
os.Exit(1)
}
appCfg, err := loadConfig()
if err == nil {
switch provider {
case "anthropic":
appCfg.Providers.Anthropic.AuthMethod = "token"
case "openai":
appCfg.Providers.OpenAI.AuthMethod = "token"
}
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
fmt.Printf("Warning: could not update config: %v\n", err)
}
}
fmt.Printf("Token saved for %s!\n", provider)
}

View file

@ -0,0 +1,64 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package auth
import (
"fmt"
"os"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/spf13/cobra"
)
var LogoutCmd = &cobra.Command{
Use: "logout",
Short: "Remove stored credentials",
Long: `Remove stored authentication credentials for a specific provider or all providers.`,
Run: func(cmd *cobra.Command, args []string) {
logoutImpl()
},
}
var logoutProvider string
func init() {
LogoutCmd.Flags().StringVarP(&logoutProvider, "provider", "p", "", "Provider to logout from (openai, anthropic)")
}
func logoutImpl() {
if logoutProvider != "" {
if err := auth.DeleteCredential(logoutProvider); err != nil {
fmt.Printf("Failed to remove credentials: %v\n", err)
os.Exit(1)
}
appCfg, err := loadConfig()
if err == nil {
switch logoutProvider {
case "openai":
appCfg.Providers.OpenAI.AuthMethod = ""
case "anthropic":
appCfg.Providers.Anthropic.AuthMethod = ""
}
config.SaveConfig(getConfigPath(), appCfg)
}
fmt.Printf("Logged out from %s\n", logoutProvider)
} else {
if err := auth.DeleteAllCredentials(); err != nil {
fmt.Printf("Failed to remove credentials: %v\n", err)
os.Exit(1)
}
appCfg, err := loadConfig()
if err == nil {
appCfg.Providers.OpenAI.AuthMethod = ""
appCfg.Providers.Anthropic.AuthMethod = ""
config.SaveConfig(getConfigPath(), appCfg)
}
fmt.Println("Logged out from all providers")
}
}

View file

@ -0,0 +1,55 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package auth
import (
"fmt"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/spf13/cobra"
)
var StatusCmd = &cobra.Command{
Use: "status",
Short: "Show current auth status",
Long: `Display the current authentication status for all providers.`,
Run: func(cmd *cobra.Command, args []string) {
statusImpl()
},
}
func statusImpl() {
store, err := auth.LoadStore()
if err != nil {
fmt.Printf("Error loading auth store: %v\n", err)
return
}
if len(store.Credentials) == 0 {
fmt.Println("No authenticated providers.")
fmt.Println("Run: picoclaw auth login --provider <name>")
return
}
fmt.Println("\nAuthenticated Providers:")
fmt.Println("------------------------")
for provider, cred := range store.Credentials {
status := "active"
if cred.IsExpired() {
status = "expired"
} else if cred.NeedsRefresh() {
status = "needs refresh"
}
fmt.Printf(" %s:\n", provider)
fmt.Printf(" Method: %s\n", cred.AuthMethod)
fmt.Printf(" Status: %s\n", status)
if cred.AccountID != "" {
fmt.Printf(" Account: %s\n", cred.AccountID)
}
if !cred.ExpiresAt.IsZero() {
fmt.Printf(" Expires: %s\n", cred.ExpiresAt.Format("2006-01-02 15:04"))
}
}
}

56
cmd/picoclaw/cron.go Normal file
View file

@ -0,0 +1,56 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"github.com/sipeed/picoclaw/cmd/picoclaw/cronpkg"
"github.com/spf13/cobra"
)
var cronCmd = &cobra.Command{
Use: "cron",
Short: "Manage scheduled tasks",
Long: `Manage cron jobs and scheduled tasks.`,
Run: func(cmd *cobra.Command, args []string) {
cronHelp()
},
}
func init() {
// PreRun to load config and store cronStorePath for subcommands
cronCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
cronpkg.SetCronStorePath(cfg.WorkspacePath())
}
cronCmd.AddCommand(cronpkg.ListCmd)
cronCmd.AddCommand(cronpkg.AddCmd)
cronCmd.AddCommand(cronpkg.RemoveCmd)
cronCmd.AddCommand(cronpkg.EnableCmd)
cronCmd.AddCommand(cronpkg.DisableCmd)
}
func cronHelp() {
fmt.Println("\nCron commands:")
fmt.Println(" list List all scheduled jobs")
fmt.Println(" add Add a new scheduled job")
fmt.Println(" remove <id> Remove a job by ID")
fmt.Println(" enable <id> Enable a job")
fmt.Println(" disable <id> Disable a job")
fmt.Println()
fmt.Println("Add options:")
fmt.Println(" -n, --name Job name")
fmt.Println(" -m, --message Message for agent")
fmt.Println(" -e, --every Run every N seconds")
fmt.Println(" -c, --cron Cron expression (e.g. '0 9 * * *')")
fmt.Println(" -d, --deliver Deliver response to channel")
fmt.Println(" --to Recipient for delivery")
fmt.Println(" --channel Channel for delivery")
}

View file

@ -0,0 +1,78 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package cronpkg
import (
"fmt"
"os"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/spf13/cobra"
)
var AddCmd = &cobra.Command{
Use: "add",
Short: "Add a new scheduled job",
Long: `Add a new cron job with specified schedule and message.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if cronName == "" {
return fmt.Errorf("--name is required")
}
if cronMessage == "" {
return fmt.Errorf("--message is required")
}
if cronEvery == 0 && cronCronExpr == "" {
return fmt.Errorf("Either --every or --cron must be specified")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
addImpl()
},
}
var (
cronName string
cronMessage string
cronEvery int64
cronCronExpr string
cronDeliver bool
cronTo string
cronChannel string
)
func init() {
AddCmd.Flags().StringVarP(&cronName, "name", "n", "", "Job name (required)")
AddCmd.Flags().StringVarP(&cronMessage, "message", "m", "", "Message for agent (required)")
AddCmd.Flags().StringVarP(&cronCronExpr, "cron", "c", "", "Cron expression (e.g. '0 9 * * *')")
AddCmd.Flags().StringVarP(&cronTo, "to", "", "", "Recipient for delivery")
AddCmd.Flags().StringVar(&cronChannel, "channel", "", "Channel for delivery")
AddCmd.Flags().Int64VarP(&cronEvery, "every", "e", 0, "Run every N seconds")
AddCmd.Flags().BoolVarP(&cronDeliver, "deliver", "d", false, "Deliver response to channel")
}
func addImpl() {
var schedule cron.CronSchedule
if cronEvery > 0 {
everyMS := cronEvery * 1000
schedule = cron.CronSchedule{
Kind: "every",
EveryMS: &everyMS,
}
} else {
schedule = cron.CronSchedule{
Kind: "cron",
Expr: cronCronExpr,
}
}
cs := cron.NewCronService(cronStorePath, nil)
job, err := cs.AddJob(cronName, schedule, cronMessage, cronDeliver, cronChannel, cronTo)
if err != nil {
fmt.Printf("Error adding job: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID)
}

View file

@ -0,0 +1,14 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package cronpkg
var cronStorePath string
func SetCronStorePath(workspace string) {
cronStorePath = workspace + "/cron/jobs.json"
}
func GetCronStorePath() string {
return cronStorePath
}

View file

@ -0,0 +1,47 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package cronpkg
import (
"fmt"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/spf13/cobra"
)
var EnableCmd = &cobra.Command{
Use: "enable <job_id>",
Short: "Enable a job",
Long: `Enable a scheduled job by its ID.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
enableImpl(args[0], false)
},
}
var DisableCmd = &cobra.Command{
Use: "disable <job_id>",
Short: "Disable a job",
Long: `Disable a scheduled job by its ID.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
enableImpl(args[0], true)
},
}
func enableImpl(jobID string, disable bool) {
cs := cron.NewCronService(cronStorePath, nil)
enabled := !disable
job := cs.EnableJob(jobID, enabled)
if job != nil {
status := "enabled"
if disable {
status = "disabled"
}
fmt.Printf("✓ Job '%s' %s\n", job.Name, status)
} else {
fmt.Printf("✗ Job %s not found\n", jobID)
}
}

View file

@ -0,0 +1,60 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package cronpkg
import (
"fmt"
"time"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/spf13/cobra"
)
var ListCmd = &cobra.Command{
Use: "list",
Short: "List all scheduled jobs",
Long: `Display all cron jobs including disabled ones.`,
Run: func(cmd *cobra.Command, args []string) {
listImpl()
},
}
func listImpl() {
cs := cron.NewCronService(cronStorePath, nil)
jobs := cs.ListJobs(true) // Show all jobs, including disabled
if len(jobs) == 0 {
fmt.Println("No scheduled jobs.")
return
}
fmt.Println("\nScheduled Jobs:")
fmt.Println("----------------")
for _, job := range jobs {
var schedule string
if job.Schedule.Kind == "every" && job.Schedule.EveryMS != nil {
schedule = fmt.Sprintf("every %ds", *job.Schedule.EveryMS/1000)
} else if job.Schedule.Kind == "cron" {
schedule = job.Schedule.Expr
} else {
schedule = "one-time"
}
nextRun := "scheduled"
if job.State.NextRunAtMS != nil {
nextTime := time.UnixMilli(*job.State.NextRunAtMS)
nextRun = nextTime.Format("2006-01-02 15:04")
}
status := "enabled"
if !job.Enabled {
status = "disabled"
}
fmt.Printf(" %s (%s)\n", job.Name, job.ID)
fmt.Printf(" Schedule: %s\n", schedule)
fmt.Printf(" Status: %s\n", status)
fmt.Printf(" Next run: %s\n", nextRun)
}
}

View file

@ -0,0 +1,30 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package cronpkg
import (
"fmt"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/spf13/cobra"
)
var RemoveCmd = &cobra.Command{
Use: "remove <job_id>",
Short: "Remove a job by ID",
Long: `Remove a scheduled job by its ID.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
removeImpl(args[0])
},
}
func removeImpl(jobID string) {
cs := cron.NewCronService(cronStorePath, nil)
if cs.RemoveJob(jobID) {
fmt.Printf("✓ Removed job %s\n", jobID)
} else {
fmt.Printf("✗ Job %s not found\n", jobID)
}
}

213
cmd/picoclaw/gateway.go Normal file
View file

@ -0,0 +1,213 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/sipeed/picoclaw/pkg/devices"
"github.com/sipeed/picoclaw/pkg/heartbeat"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/voice"
"github.com/spf13/cobra"
)
var gatewayCmd = &cobra.Command{
Use: "gateway",
Short: "Start picoclaw gateway",
Long: `Start the picoclaw gateway service with all channels and services.`,
Run: func(cmd *cobra.Command, args []string) {
gatewayImpl()
},
}
var gatewayDebug bool
func init() {
gatewayCmd.Flags().BoolVarP(&gatewayDebug, "debug", "d", false, "Enable debug mode")
}
func gatewayImpl() {
if gatewayDebug {
logger.SetLevel(logger.DEBUG)
fmt.Println("🔍 Debug mode enabled")
}
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
provider, err := providers.CreateProvider(cfg)
if err != nil {
fmt.Printf("Error creating provider: %v\n", err)
os.Exit(1)
}
msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
// Print agent startup info
fmt.Println("\n📦 Agent Status:")
startupInfo := agentLoop.GetStartupInfo()
toolsInfo := startupInfo["tools"].(map[string]interface{})
skillsInfo := startupInfo["skills"].(map[string]interface{})
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
fmt.Printf(" • Skills: %d/%d available\n",
skillsInfo["available"],
skillsInfo["total"])
// Log to file as well
logger.InfoCF("agent", "Agent initialized",
map[string]interface{}{
"tools_count": toolsInfo["count"],
"skills_total": skillsInfo["total"],
"skills_available": skillsInfo["available"],
})
// Setup cron tool and service
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath())
heartbeatService := heartbeat.NewHeartbeatService(
cfg.WorkspacePath(),
cfg.Heartbeat.Interval,
cfg.Heartbeat.Enabled,
)
heartbeatService.SetBus(msgBus)
heartbeatService.SetHandler(func(prompt, channel, chatID string) *tools.ToolResult {
// Use cli:direct as fallback if no valid channel
if channel == "" || chatID == "" {
channel, chatID = "cli", "direct"
}
// Use ProcessHeartbeat - no session history, each heartbeat is independent
response, err := agentLoop.ProcessHeartbeat(context.Background(), prompt, channel, chatID)
if err != nil {
return tools.ErrorResult(fmt.Sprintf("Heartbeat error: %v", err))
}
if response == "HEARTBEAT_OK" {
return tools.SilentResult("Heartbeat OK")
}
// For heartbeat, always return silent - the subagent result will be
// sent to user via processSystemMessage when the async task completes
return tools.SilentResult(response)
})
channelManager, err := channels.NewManager(cfg, msgBus)
if err != nil {
fmt.Printf("Error creating channel manager: %v\n", err)
os.Exit(1)
}
var transcriber *voice.GroqTranscriber
if cfg.Providers.Groq.APIKey != "" {
transcriber = voice.NewGroqTranscriber(cfg.Providers.Groq.APIKey)
logger.InfoC("voice", "Groq voice transcription enabled")
}
if transcriber != nil {
if telegramChannel, ok := channelManager.GetChannel("telegram"); ok {
if tc, ok := telegramChannel.(*channels.TelegramChannel); ok {
tc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Telegram channel")
}
}
if discordChannel, ok := channelManager.GetChannel("discord"); ok {
if dc, ok := discordChannel.(*channels.DiscordChannel); ok {
dc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Discord channel")
}
}
if slackChannel, ok := channelManager.GetChannel("slack"); ok {
if sc, ok := slackChannel.(*channels.SlackChannel); ok {
sc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Slack channel")
}
}
}
enabledChannels := channelManager.GetEnabledChannels()
if len(enabledChannels) > 0 {
fmt.Printf("✓ Channels enabled: %s\n", enabledChannels)
} else {
fmt.Println("⚠ Warning: No channels enabled")
}
fmt.Printf("✓ Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
fmt.Println("Press Ctrl+C to stop")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := cronService.Start(); err != nil {
fmt.Printf("Error starting cron service: %v\n", err)
}
fmt.Println("✓ Cron service started")
if err := heartbeatService.Start(); err != nil {
fmt.Printf("Error starting heartbeat service: %v\n", err)
}
fmt.Println("✓ Heartbeat service started")
stateManager := state.NewManager(cfg.WorkspacePath())
deviceService := devices.NewService(devices.Config{
Enabled: cfg.Devices.Enabled,
MonitorUSB: cfg.Devices.MonitorUSB,
}, stateManager)
deviceService.SetBus(msgBus)
if err := deviceService.Start(ctx); err != nil {
fmt.Printf("Error starting device service: %v\n", err)
} else if cfg.Devices.Enabled {
fmt.Println("✓ Device event service started")
}
if err := channelManager.StartAll(ctx); err != nil {
fmt.Printf("Error starting channels: %v\n", err)
}
go agentLoop.Run(ctx)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
<-sigChan
fmt.Println("\nShutting down...")
cancel()
deviceService.Stop()
heartbeatService.Stop()
cronService.Stop()
agentLoop.Stop()
channelManager.StopAll(ctx)
fmt.Println("✓ Gateway stopped")
}
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string) *cron.CronService {
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
// Create cron service
cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace)
agentLoop.RegisterTool(cronTool)
// Set the onJob handler
cronService.SetOnJob(func(job *cron.CronJob) (string, error) {
result := cronTool.ExecuteJob(context.Background(), job)
return result, nil
})
return cronService
}

File diff suppressed because it is too large Load diff

63
cmd/picoclaw/migrate.go Normal file
View file

@ -0,0 +1,63 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"os"
"github.com/sipeed/picoclaw/pkg/migrate"
"github.com/spf13/cobra"
)
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate from OpenClaw to PicoClaw",
Long: `Migrate configuration and workspace from OpenClaw to PicoClaw.`,
Run: func(cmd *cobra.Command, args []string) {
migrateImpl()
},
}
var (
migrateDryRun bool
migrateRefresh bool
migrateConfigOnly bool
migrateWorkspaceOnly bool
migrateForce bool
migrateOpenClawHome string
migratePicoClawHome string
)
func init() {
migrateCmd.Flags().BoolVar(&migrateDryRun, "dry-run", false, "Show what would be migrated without making changes")
migrateCmd.Flags().BoolVar(&migrateRefresh, "refresh", false, "Re-sync workspace files from OpenClaw (repeatable)")
migrateCmd.Flags().BoolVar(&migrateConfigOnly, "config-only", false, "Only migrate config, skip workspace files")
migrateCmd.Flags().BoolVar(&migrateWorkspaceOnly, "workspace-only", false, "Only migrate workspace files, skip config")
migrateCmd.Flags().BoolVar(&migrateForce, "force", false, "Skip confirmation prompts")
migrateCmd.Flags().StringVar(&migrateOpenClawHome, "openclaw-home", "", "Override OpenClaw home directory (default: ~/.openclaw)")
migrateCmd.Flags().StringVar(&migratePicoClawHome, "picoclaw-home", "", "Override PicoClaw home directory (default: ~/.picoclaw)")
}
func migrateImpl() {
opts := migrate.Options{
DryRun: migrateDryRun,
Refresh: migrateRefresh,
ConfigOnly: migrateConfigOnly,
WorkspaceOnly: migrateWorkspaceOnly,
Force: migrateForce,
OpenClawHome: migrateOpenClawHome,
PicoClawHome: migratePicoClawHome,
}
result, err := migrate.Run(opts)
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
if !opts.DryRun {
migrate.PrintSummary(result)
}
}

167
cmd/picoclaw/onboard.go Normal file
View file

@ -0,0 +1,167 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"embed"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/spf13/cobra"
)
//go:embed workspace
var embeddedFiles embed.FS
var onboardCmd = &cobra.Command{
Use: "onboard",
Short: "Initialize picoclaw configuration and workspace",
Long: "Initialize picoclaw configuration and workspace for first-time use.",
Run: func(cmd *cobra.Command, args []string) {
onboard()
},
}
func onboard() {
configPath := getConfigPath()
if _, err := os.Stat(configPath); err == nil {
fmt.Printf("Config already exists at %s\n", configPath)
fmt.Print("Overwrite? (y/n): ")
var response string
fmt.Scanln(&response)
if response != "y" {
fmt.Println("Aborted.")
return
}
}
cfg := config.DefaultConfig()
if err := config.SaveConfig(configPath, cfg); err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}
workspace := cfg.WorkspacePath()
createWorkspaceTemplates(workspace)
fmt.Printf("%s picoclaw is ready!\n", logo)
fmt.Println("\nNext steps:")
fmt.Println(" 1. Add your API key to", configPath)
fmt.Println(" Get one at: https://openrouter.ai/keys")
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
}
func copyDirectory(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer dstFile.Close()
_, err = fmtCopy(dstFile, srcFile)
return err
})
}
func fmtCopy(dst *os.File, src *os.File) (int64, error) {
buf := make([]byte, 32*1024)
var written int64
for {
n, err := src.Read(buf)
if n > 0 {
wn, err := dst.Write(buf[:n])
if err != nil {
return written, err
}
written += int64(wn)
}
if err != nil {
if err == io.EOF {
return written, nil
}
return written, err
}
}
}
func copyEmbeddedToTarget(targetDir string) error {
// Ensure target directory exists
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("Failed to create target directory: %w", err)
}
// Walk through all files in embed.FS
err := fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip directories
if d.IsDir() {
return nil
}
// Read embedded file
data, err := embeddedFiles.ReadFile(path)
if err != nil {
return fmt.Errorf("Failed to read embedded file %s: %w", path, err)
}
newPath, err := filepath.Rel("workspace", path)
if err != nil {
return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err)
}
// Build target file path
targetPath := filepath.Join(targetDir, newPath)
// Ensure target file's directory exists
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
}
// Write file
if err := os.WriteFile(targetPath, data, 0644); err != nil {
return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
}
return nil
})
return err
}
func createWorkspaceTemplates(workspace string) {
err := copyEmbeddedToTarget(workspace)
if err != nil {
fmt.Printf("Error copying workspace templates: %v\n", err)
}
}

59
cmd/picoclaw/root.go Normal file
View file

@ -0,0 +1,59 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var showVersion bool
var rootCmd = &cobra.Command{
Use: "picoclaw",
Short: "Personal AI Assistant",
Long: `PicoClaw - Ultra-lightweight personal AI agent.
A simple and powerful AI assistant that runs on your local machine.`,
Run: func(cmd *cobra.Command, args []string) {
if showVersion {
printVersion()
return
}
printHelp()
os.Exit(1)
},
}
func printHelp() {
fmt.Printf("%s picoclaw - Personal AI Assistant v%s\n\n", logo, version)
fmt.Println("Usage: picoclaw <command>")
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" onboard Initialize picoclaw configuration and workspace")
fmt.Println(" agent Interact with the agent directly")
fmt.Println(" auth Manage authentication (login, logout, status)")
fmt.Println(" gateway Start picoclaw gateway")
fmt.Println(" status Show picoclaw status")
fmt.Println(" cron Manage scheduled tasks")
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw")
fmt.Println(" skills Manage skills (install, list, remove)")
fmt.Println(" version Show version information")
}
func init() {
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information")
// Add subcommands
rootCmd.AddCommand(onboardCmd)
rootCmd.AddCommand(agentCmd)
rootCmd.AddCommand(gatewayCmd)
rootCmd.AddCommand(statusCmd)
rootCmd.AddCommand(migrateCmd)
rootCmd.AddCommand(authCmd)
rootCmd.AddCommand(cronCmd)
rootCmd.AddCommand(skillsCmd)
rootCmd.AddCommand(versionCmd)
}

67
cmd/picoclaw/skills.go Normal file
View file

@ -0,0 +1,67 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"path/filepath"
"github.com/sipeed/picoclaw/cmd/picoclaw/skillspkg"
"github.com/spf13/cobra"
)
var skillsCmd = &cobra.Command{
Use: "skills",
Short: "Manage skills",
Long: `Manage skills installation and listing.`,
Run: func(cmd *cobra.Command, args []string) {
skillsHelp()
},
}
func init() {
// PreRun to load config and create installer/loader for subcommands
skillsCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
workspace := cfg.WorkspacePath()
// 获取全局配置目录和内置 skills 目录
globalDir := filepath.Dir(getConfigPath())
globalSkillsDir := filepath.Join(globalDir, "skills")
builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills")
skillspkg.SetWorkspace(workspace)
skillspkg.SetGlobalDirs(globalSkillsDir, builtinSkillsDir)
}
skillsCmd.AddCommand(skillspkg.ListCmd)
skillsCmd.AddCommand(skillspkg.InstallCmd)
skillsCmd.AddCommand(skillspkg.RemoveCmd)
skillsCmd.AddCommand(skillspkg.InstallBuiltinCmd)
skillsCmd.AddCommand(skillspkg.ListBuiltinCmd)
skillsCmd.AddCommand(skillspkg.SearchCmd)
skillsCmd.AddCommand(skillspkg.ShowCmd)
}
func skillsHelp() {
fmt.Println("\nSkills commands:")
fmt.Println(" list List installed skills")
fmt.Println(" install <repo> Install skill from GitHub")
fmt.Println(" install-builtin Install all builtin skills to workspace")
fmt.Println(" list-builtin List available builtin skills")
fmt.Println(" remove <name> Remove installed skill")
fmt.Println(" search Search available skills")
fmt.Println(" show <name> Show skill details")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" picoclaw skills list")
fmt.Println(" picoclaw skills install sipeed/picoclaw-skills/weather")
fmt.Println(" picoclaw skills install-builtin")
fmt.Println(" picoclaw skills list-builtin")
fmt.Println(" picoclaw skills remove weather")
}

View file

@ -0,0 +1,39 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
)
var InstallCmd = &cobra.Command{
Use: "install <repo>",
Short: "Install skill from GitHub",
Long: `Install a skill from a GitHub repository.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
installImpl(args[0])
},
}
func installImpl(repo string) {
fmt.Printf("Installing skill from %s...\n", repo)
installer := getInstaller()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := installer.InstallFromGitHub(ctx, repo); err != nil {
fmt.Printf("✗ Failed to install skill: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Skill '%s' installed successfully!\n", filepath.Base(repo))
}

View file

@ -0,0 +1,113 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
var InstallBuiltinCmd = &cobra.Command{
Use: "install-builtin",
Short: "Install all builtin skills to workspace",
Long: `Copy all builtin skills from the global skills directory to the workspace.`,
Run: func(cmd *cobra.Command, args []string) {
installBuiltinImpl()
},
}
func installBuiltinImpl() {
builtinSkillsDir := getBuiltinSkillsDir()
workspaceSkillsDir := filepath.Join(getWorkspace(), "skills")
fmt.Printf("Copying builtin skills to workspace...\n")
skillsToInstall := []string{
"weather",
"news",
"stock",
"calculator",
}
for _, skillName := range skillsToInstall {
builtinPath := filepath.Join(builtinSkillsDir, skillName)
workspacePath := filepath.Join(workspaceSkillsDir, skillName)
if _, err := os.Stat(builtinPath); err != nil {
fmt.Printf("⊘ Builtin skill '%s' not found: %v\n", skillName, err)
continue
}
if err := os.MkdirAll(workspacePath, 0755); err != nil {
fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err)
continue
}
if err := copyDirectory(builtinPath, workspacePath); err != nil {
fmt.Printf("✗ Failed to copy %s: %v\n", skillName, err)
}
}
fmt.Println("\n✓ All builtin skills installed!")
fmt.Println("Now you can use them in your workspace.")
}
func copyDirectory(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer dstFile.Close()
_, err = fmtCopy(dstFile, srcFile)
return err
})
}
func fmtCopy(dst *os.File, src *os.File) (int64, error) {
buf := make([]byte, 32*1024)
var written int64
for {
n, err := src.Read(buf)
if n > 0 {
wn, err := dst.Write(buf[:n])
if err != nil {
return written, err
}
written += int64(wn)
}
if err != nil {
if err == io.EOF {
return written, nil
}
return written, err
}
}
}

View file

@ -0,0 +1,38 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"fmt"
"github.com/spf13/cobra"
)
var ListCmd = &cobra.Command{
Use: "list",
Short: "List installed skills",
Long: `Display all installed skills with their descriptions.`,
Run: func(cmd *cobra.Command, args []string) {
listImpl()
},
}
func listImpl() {
loader := getLoader()
allSkills := loader.ListSkills()
if len(allSkills) == 0 {
fmt.Println("No skills installed.")
return
}
fmt.Println("\nInstalled Skills:")
fmt.Println("------------------")
for _, skill := range allSkills {
fmt.Printf(" ✓ %s (%s)\n", skill.Name, skill.Source)
if skill.Description != "" {
fmt.Printf(" %s\n", skill.Description)
}
}
}

View file

@ -0,0 +1,69 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
var ListBuiltinCmd = &cobra.Command{
Use: "list-builtin",
Short: "List available builtin skills",
Long: `Display all builtin skills that can be installed to the workspace.`,
Run: func(cmd *cobra.Command, args []string) {
listBuiltinImpl()
},
}
func listBuiltinImpl() {
builtinSkillsDir := getBuiltinSkillsDir()
fmt.Println("\nAvailable Builtin Skills:")
fmt.Println("-----------------------")
entries, err := os.ReadDir(builtinSkillsDir)
if err != nil {
fmt.Printf("Error reading builtin skills: %v\n", err)
return
}
if len(entries) == 0 {
fmt.Println("No builtin skills available.")
return
}
for _, entry := range entries {
if entry.IsDir() {
skillName := entry.Name()
skillFile := filepath.Join(builtinSkillsDir, skillName, "SKILL.md")
description := "No description"
if _, err := os.Stat(skillFile); err == nil {
data, err := os.ReadFile(skillFile)
if err == nil {
content := string(data)
if idx := strings.Index(content, "\n"); idx > 0 {
firstLine := content[:idx]
if strings.Contains(firstLine, "description:") {
descLine := strings.Index(content[idx:], "\n")
if descLine > 0 {
description = strings.TrimSpace(content[idx+descLine : idx+descLine])
}
}
}
}
}
status := "✓"
fmt.Printf(" %s %s\n", status, entry.Name())
if description != "" {
fmt.Printf(" %s\n", description)
}
}
}
}

View file

@ -0,0 +1,33 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var RemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove installed skill",
Long: `Remove an installed skill by name.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
removeImpl(args[0])
},
}
func removeImpl(skillName string) {
fmt.Printf("Removing skill '%s'...\n", skillName)
installer := getInstaller()
if err := installer.Uninstall(skillName); err != nil {
fmt.Printf("✗ Failed to remove skill: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Skill '%s' removed successfully!\n", skillName)
}

View file

@ -0,0 +1,55 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
)
var SearchCmd = &cobra.Command{
Use: "search",
Short: "Search available skills",
Long: `Search and list all available skills from the skills repository.`,
Run: func(cmd *cobra.Command, args []string) {
searchImpl()
},
}
func searchImpl() {
fmt.Println("Searching for available skills...")
installer := getInstaller()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
availableSkills, err := installer.ListAvailableSkills(ctx)
if err != nil {
fmt.Printf("✗ Failed to fetch skills list: %v\n", err)
return
}
if len(availableSkills) == 0 {
fmt.Println("No skills available.")
return
}
fmt.Printf("\nAvailable Skills (%d):\n", len(availableSkills))
fmt.Println("--------------------")
for _, skill := range availableSkills {
fmt.Printf(" 📦 %s\n", skill.Name)
fmt.Printf(" %s\n", skill.Description)
fmt.Printf(" Repo: %s\n", skill.Repository)
if skill.Author != "" {
fmt.Printf(" Author: %s\n", skill.Author)
}
if len(skill.Tags) > 0 {
fmt.Printf(" Tags: %v\n", skill.Tags)
}
fmt.Println()
}
}

View file

@ -0,0 +1,33 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"fmt"
"github.com/spf13/cobra"
)
var ShowCmd = &cobra.Command{
Use: "show <name>",
Short: "Show skill details",
Long: `Display the full content and details of an installed skill.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
showImpl(args[0])
},
}
func showImpl(skillName string) {
loader := getLoader()
content, ok := loader.LoadSkill(skillName)
if !ok {
fmt.Printf("✗ Skill '%s' not found\n", skillName)
return
}
fmt.Printf("\n📦 Skill: %s\n", skillName)
fmt.Println("----------------------")
fmt.Println(content)
}

View file

@ -0,0 +1,41 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package skillspkg
import (
"path/filepath"
"github.com/sipeed/picoclaw/pkg/skills"
)
var (
workspace string
globalSkillsDir string
builtinSkillsDir string
)
func SetWorkspace(ws string) {
workspace = ws
}
func SetGlobalDirs(global, builtin string) {
globalSkillsDir = global
builtinSkillsDir = builtin
}
func getInstaller() *skills.SkillInstaller {
return skills.NewSkillInstaller(workspace)
}
func getLoader() *skills.SkillsLoader {
return skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir)
}
func getWorkspace() string {
return workspace
}
func getBuiltinSkillsDir() string {
return filepath.Join(workspace, "../picoclaw/skills")
}

96
cmd/picoclaw/status.go Normal file
View file

@ -0,0 +1,96 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"os"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/spf13/cobra"
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "Show picoclaw status",
Long: "Display current picoclaw configuration and status information.",
Run: func(cmd *cobra.Command, args []string) {
statusCmdImpl()
},
}
func statusCmdImpl() {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
configPath := getConfigPath()
fmt.Printf("%s picoclaw Status\n", logo)
fmt.Printf("Version: %s\n", formatVersion())
build, _ := formatBuildInfo()
if build != "" {
fmt.Printf("Build: %s\n", build)
}
fmt.Println()
if _, err := os.Stat(configPath); err == nil {
fmt.Println("Config:", configPath, "✓")
} else {
fmt.Println("Config:", configPath, "✗")
}
workspace := cfg.WorkspacePath()
if _, err := os.Stat(workspace); err == nil {
fmt.Println("Workspace:", workspace, "✓")
} else {
fmt.Println("Workspace:", workspace, "✗")
}
if _, err := os.Stat(configPath); err == nil {
fmt.Printf("Model: %s\n", cfg.Agents.Defaults.Model)
hasOpenRouter := cfg.Providers.OpenRouter.APIKey != ""
hasAnthropic := cfg.Providers.Anthropic.APIKey != ""
hasOpenAI := cfg.Providers.OpenAI.APIKey != ""
hasGemini := cfg.Providers.Gemini.APIKey != ""
hasZhipu := cfg.Providers.Zhipu.APIKey != ""
hasGroq := cfg.Providers.Groq.APIKey != ""
hasVLLM := cfg.Providers.VLLM.APIBase != ""
status := func(enabled bool) string {
if enabled {
return "✓"
}
return "not set"
}
fmt.Println("OpenRouter API:", status(hasOpenRouter))
fmt.Println("Anthropic API:", status(hasAnthropic))
fmt.Println("OpenAI API:", status(hasOpenAI))
fmt.Println("Gemini API:", status(hasGemini))
fmt.Println("Zhipu API:", status(hasZhipu))
fmt.Println("Groq API:", status(hasGroq))
if hasVLLM {
fmt.Printf("vLLM/Local: ✓ %s\n", cfg.Providers.VLLM.APIBase)
} else {
fmt.Println("vLLM/Local: not set")
}
store, _ := auth.LoadStore()
if store != nil && len(store.Credentials) > 0 {
fmt.Println("\nOAuth/Token Auth:")
for provider, cred := range store.Credentials {
authStatus := "authenticated"
if cred.IsExpired() {
authStatus = "expired"
} else if cred.NeedsRefresh() {
authStatus = "needs refresh"
}
fmt.Printf(" %s (%s): %s\n", provider, cred.AuthMethod, authStatus)
}
}
}
}

59
cmd/picoclaw/utils.go Normal file
View file

@ -0,0 +1,59 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/sipeed/picoclaw/pkg/config"
)
var (
version = "dev"
gitCommit string
buildTime string
goVersion string
logo = "🦞"
)
// formatVersion returns the version string with optional git commit
func formatVersion() string {
v := version
if gitCommit != "" {
v += fmt.Sprintf(" (git: %s)", gitCommit)
}
return v
}
// formatBuildInfo returns build time and go version info
func formatBuildInfo() (build string, goVer string) {
if buildTime != "" {
build = buildTime
}
goVer = goVersion
if goVer == "" {
goVer = runtime.Version()
}
return
}
// getConfigPath returns the path to the config file
func getConfigPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "config.json")
}
// loadConfig loads the configuration from the default path
func loadConfig() (*config.Config, error) {
return config.LoadConfig(getConfigPath())
}
// exitOnError prints an error message and exits with status 1
func exitOnError(format string, args ...interface{}) {
fmt.Printf(format+"\n", args...)
os.Exit(1)
}

30
cmd/picoclaw/version.go Normal file
View file

@ -0,0 +1,30 @@
// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package main
import (
"fmt"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show version information",
Long: "Display PicoClaw version and build information.",
Run: func(cmd *cobra.Command, args []string) {
printVersion()
},
}
func printVersion() {
fmt.Printf("%s picoclaw %s\n", logo, formatVersion())
build, goVer := formatBuildInfo()
if build != "" {
fmt.Printf(" Build: %s\n", build)
}
if goVer != "" {
fmt.Printf(" Go: %s\n", goVer)
}
}

3
go.mod
View file

@ -15,6 +15,7 @@ require (
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
github.com/openai/openai-go/v3 v3.22.0
github.com/slack-go/slack v0.17.3
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/tencent-connect/botgo v0.2.1
golang.org/x/oauth2 v0.35.0
@ -22,7 +23,9 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

9
go.sum
View file

@ -25,6 +25,7 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -72,6 +73,8 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
@ -108,8 +111,13 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -151,6 +159,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=