refactor main.go with cobra
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
This commit is contained in:
parent
56a060ff61
commit
2b9b7720e3
30 changed files with 1935 additions and 1421 deletions
175
cmd/picoclaw/agent.go
Normal file
175
cmd/picoclaw/agent.go
Normal 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
44
cmd/picoclaw/auth.go
Normal 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
124
cmd/picoclaw/auth/login.go
Normal 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)
|
||||
}
|
||||
64
cmd/picoclaw/auth/logout.go
Normal file
64
cmd/picoclaw/auth/logout.go
Normal 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")
|
||||
}
|
||||
}
|
||||
55
cmd/picoclaw/auth/status.go
Normal file
55
cmd/picoclaw/auth/status.go
Normal 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
56
cmd/picoclaw/cron.go
Normal 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")
|
||||
}
|
||||
78
cmd/picoclaw/cronpkg/add.go
Normal file
78
cmd/picoclaw/cronpkg/add.go
Normal 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)
|
||||
}
|
||||
14
cmd/picoclaw/cronpkg/cron.go
Normal file
14
cmd/picoclaw/cronpkg/cron.go
Normal 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
|
||||
}
|
||||
47
cmd/picoclaw/cronpkg/enable.go
Normal file
47
cmd/picoclaw/cronpkg/enable.go
Normal 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)
|
||||
}
|
||||
}
|
||||
60
cmd/picoclaw/cronpkg/list.go
Normal file
60
cmd/picoclaw/cronpkg/list.go
Normal 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)
|
||||
}
|
||||
}
|
||||
30
cmd/picoclaw/cronpkg/remove.go
Normal file
30
cmd/picoclaw/cronpkg/remove.go
Normal 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
213
cmd/picoclaw/gateway.go
Normal 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
|
||||
}
|
||||
1422
cmd/picoclaw/main.go
1422
cmd/picoclaw/main.go
File diff suppressed because it is too large
Load diff
63
cmd/picoclaw/migrate.go
Normal file
63
cmd/picoclaw/migrate.go
Normal 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
167
cmd/picoclaw/onboard.go
Normal 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
59
cmd/picoclaw/root.go
Normal 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
67
cmd/picoclaw/skills.go
Normal 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")
|
||||
}
|
||||
39
cmd/picoclaw/skillspkg/install.go
Normal file
39
cmd/picoclaw/skillspkg/install.go
Normal 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))
|
||||
}
|
||||
113
cmd/picoclaw/skillspkg/install_builtin.go
Normal file
113
cmd/picoclaw/skillspkg/install_builtin.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
38
cmd/picoclaw/skillspkg/list.go
Normal file
38
cmd/picoclaw/skillspkg/list.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
cmd/picoclaw/skillspkg/list_builtin.go
Normal file
69
cmd/picoclaw/skillspkg/list_builtin.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
cmd/picoclaw/skillspkg/remove.go
Normal file
33
cmd/picoclaw/skillspkg/remove.go
Normal 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)
|
||||
}
|
||||
55
cmd/picoclaw/skillspkg/search.go
Normal file
55
cmd/picoclaw/skillspkg/search.go
Normal 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()
|
||||
}
|
||||
}
|
||||
33
cmd/picoclaw/skillspkg/show.go
Normal file
33
cmd/picoclaw/skillspkg/show.go
Normal 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)
|
||||
}
|
||||
41
cmd/picoclaw/skillspkg/skills.go
Normal file
41
cmd/picoclaw/skillspkg/skills.go
Normal 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
96
cmd/picoclaw/status.go
Normal 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
59
cmd/picoclaw/utils.go
Normal 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
30
cmd/picoclaw/version.go
Normal 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
3
go.mod
|
|
@ -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
9
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue