refactor: parsing cli arguments use cobra
Signed-off-by: Kai Xia <kaix+github@fastmail.com>
This commit is contained in:
parent
1ef33c90ed
commit
446b4eca23
11 changed files with 697 additions and 665 deletions
|
|
@ -17,35 +17,31 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func agentCmd() {
|
||||
message := ""
|
||||
sessionKey := "cli:default"
|
||||
modelOverride := ""
|
||||
func newAgentCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "agent",
|
||||
Short: "Interact with the agent directly",
|
||||
RunE: runAgent,
|
||||
}
|
||||
cmd.Flags().BoolP("debug", "d", false, "Enable debug mode")
|
||||
cmd.Flags().StringP("message", "m", "", "Send a single message")
|
||||
cmd.Flags().StringP("session", "s", "cli:default", "Session key")
|
||||
cmd.Flags().String("model", "", "Override model")
|
||||
return cmd
|
||||
}
|
||||
|
||||
args := os.Args[2:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--debug", "-d":
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
case "-m", "--message":
|
||||
if i+1 < len(args) {
|
||||
message = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "-s", "--session":
|
||||
if i+1 < len(args) {
|
||||
sessionKey = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--model", "-model":
|
||||
if i+1 < len(args) {
|
||||
modelOverride = args[i+1]
|
||||
i++
|
||||
}
|
||||
}
|
||||
func runAgent(cmd *cobra.Command, args []string) error {
|
||||
debug, _ := cmd.Flags().GetBool("debug")
|
||||
message, _ := cmd.Flags().GetString("message")
|
||||
sessionKey, _ := cmd.Flags().GetString("session")
|
||||
modelOverride, _ := cmd.Flags().GetString("model")
|
||||
|
||||
if debug {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("\U0001f50d Debug mode enabled")
|
||||
}
|
||||
|
||||
cfg, err := loadConfig()
|
||||
|
|
@ -63,7 +59,6 @@ func agentCmd() {
|
|||
fmt.Printf("Error creating provider: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Use the resolved model ID from provider creation
|
||||
if modelID != "" {
|
||||
cfg.Agents.Defaults.Model = modelID
|
||||
}
|
||||
|
|
@ -71,7 +66,6 @@ func agentCmd() {
|
|||
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{}{
|
||||
|
|
@ -92,6 +86,7 @@ func agentCmd() {
|
|||
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
|
||||
interactiveMode(agentLoop, sessionKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
|
|
|
|||
|
|
@ -15,74 +15,66 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/auth"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const supportedProvidersMsg = "Supported providers: openai, anthropic, google-antigravity"
|
||||
|
||||
func authCmd() {
|
||||
if len(os.Args) < 3 {
|
||||
authHelp()
|
||||
return
|
||||
func newAuthCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Manage authentication (login, logout, status)",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newAuthLoginCmd(),
|
||||
newAuthLogoutCmd(),
|
||||
newAuthStatusCmd(),
|
||||
newAuthModelsCmd(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
switch os.Args[2] {
|
||||
case "login":
|
||||
authLoginCmd()
|
||||
case "logout":
|
||||
authLogoutCmd()
|
||||
case "status":
|
||||
authStatusCmd()
|
||||
case "models":
|
||||
authModelsCmd()
|
||||
default:
|
||||
fmt.Printf("Unknown auth command: %s\n", os.Args[2])
|
||||
authHelp()
|
||||
func newAuthLoginCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Login via OAuth or paste token",
|
||||
RunE: runAuthLogin,
|
||||
}
|
||||
cmd.Flags().StringP("provider", "p", "", "Provider (openai, anthropic, google-antigravity)")
|
||||
cmd.MarkFlagRequired("provider")
|
||||
cmd.Flags().Bool("device-code", false, "Use device code flow (for headless environments)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newAuthLogoutCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Remove stored credentials",
|
||||
RunE: runAuthLogout,
|
||||
}
|
||||
cmd.Flags().StringP("provider", "p", "", "Provider to logout from (omit for all)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newAuthStatusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show current auth status",
|
||||
RunE: runAuthStatus,
|
||||
}
|
||||
}
|
||||
|
||||
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(" models List available Antigravity models")
|
||||
fmt.Println()
|
||||
fmt.Println("Login options:")
|
||||
fmt.Println(" --provider <name> Provider to login with (openai, anthropic, google-antigravity)")
|
||||
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 login --provider google-antigravity")
|
||||
fmt.Println(" picoclaw auth models")
|
||||
fmt.Println(" picoclaw auth logout --provider openai")
|
||||
fmt.Println(" picoclaw auth status")
|
||||
func newAuthModelsCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "models",
|
||||
Short: "List available Antigravity models",
|
||||
RunE: runAuthModels,
|
||||
}
|
||||
}
|
||||
|
||||
func authLoginCmd() {
|
||||
provider := ""
|
||||
useDeviceCode := false
|
||||
|
||||
args := os.Args[3:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--provider", "-p":
|
||||
if i+1 < len(args) {
|
||||
provider = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--device-code":
|
||||
useDeviceCode = true
|
||||
}
|
||||
}
|
||||
|
||||
if provider == "" {
|
||||
fmt.Println("Error: --provider is required")
|
||||
fmt.Println(supportedProvidersMsg)
|
||||
return
|
||||
}
|
||||
func runAuthLogin(cmd *cobra.Command, args []string) error {
|
||||
provider, _ := cmd.Flags().GetString("provider")
|
||||
useDeviceCode, _ := cmd.Flags().GetBool("device-code")
|
||||
|
||||
switch provider {
|
||||
case "openai":
|
||||
|
|
@ -95,6 +87,162 @@ func authLoginCmd() {
|
|||
fmt.Printf("Unsupported provider: %s\n", provider)
|
||||
fmt.Println(supportedProvidersMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAuthLogout(cmd *cobra.Command, args []string) error {
|
||||
provider, _ := cmd.Flags().GetString("provider")
|
||||
|
||||
if provider != "" {
|
||||
if err := auth.DeleteCredential(provider); err != nil {
|
||||
fmt.Printf("Failed to remove credentials: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appCfg, err := loadConfig()
|
||||
if err == nil {
|
||||
for i := range appCfg.ModelList {
|
||||
switch provider {
|
||||
case "openai":
|
||||
if isOpenAIModel(appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
case "anthropic":
|
||||
if isAnthropicModel(appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
case "google-antigravity", "antigravity":
|
||||
if isAntigravityModel(appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
switch provider {
|
||||
case "openai":
|
||||
appCfg.Providers.OpenAI.AuthMethod = ""
|
||||
case "anthropic":
|
||||
appCfg.Providers.Anthropic.AuthMethod = ""
|
||||
case "google-antigravity", "antigravity":
|
||||
appCfg.Providers.Antigravity.AuthMethod = ""
|
||||
}
|
||||
config.SaveConfig(getConfigPath(), appCfg)
|
||||
}
|
||||
|
||||
fmt.Printf("Logged out from %s\n", provider)
|
||||
} 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 {
|
||||
for i := range appCfg.ModelList {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
appCfg.Providers.OpenAI.AuthMethod = ""
|
||||
appCfg.Providers.Anthropic.AuthMethod = ""
|
||||
appCfg.Providers.Antigravity.AuthMethod = ""
|
||||
config.SaveConfig(getConfigPath(), appCfg)
|
||||
}
|
||||
|
||||
fmt.Println("Logged out from all providers")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAuthStatus(cmd *cobra.Command, args []string) error {
|
||||
store, err := auth.LoadStore()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading auth store: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(store.Credentials) == 0 {
|
||||
fmt.Println("No authenticated providers.")
|
||||
fmt.Println("Run: picoclaw auth login --provider <name>")
|
||||
return nil
|
||||
}
|
||||
|
||||
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.Email != "" {
|
||||
fmt.Printf(" Email: %s\n", cred.Email)
|
||||
}
|
||||
if cred.ProjectID != "" {
|
||||
fmt.Printf(" Project: %s\n", cred.ProjectID)
|
||||
}
|
||||
if !cred.ExpiresAt.IsZero() {
|
||||
fmt.Printf(" Expires: %s\n", cred.ExpiresAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAuthModels(cmd *cobra.Command, args []string) error {
|
||||
cred, err := auth.GetCredential("google-antigravity")
|
||||
if err != nil || cred == nil {
|
||||
fmt.Println("Not logged in to Google Antigravity.")
|
||||
fmt.Println("Run: picoclaw auth login --provider google-antigravity")
|
||||
return nil
|
||||
}
|
||||
|
||||
if cred.NeedsRefresh() && cred.RefreshToken != "" {
|
||||
oauthCfg := auth.GoogleAntigravityOAuthConfig()
|
||||
refreshed, refreshErr := auth.RefreshAccessToken(cred, oauthCfg)
|
||||
if refreshErr == nil {
|
||||
cred = refreshed
|
||||
_ = auth.SetCredential("google-antigravity", cred)
|
||||
}
|
||||
}
|
||||
|
||||
projectID := cred.ProjectID
|
||||
if projectID == "" {
|
||||
fmt.Println("No project ID stored. Try logging in again.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Fetching models for project: %s\n\n", projectID)
|
||||
|
||||
models, err := providers.FetchAntigravityModels(cred.AccessToken, projectID)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching models: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
fmt.Println("No models available.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Available Antigravity Models:")
|
||||
fmt.Println("-----------------------------")
|
||||
for _, m := range models {
|
||||
status := "\u2713"
|
||||
if m.IsExhausted {
|
||||
status = "\u2717 (quota exhausted)"
|
||||
}
|
||||
name := m.ID
|
||||
if m.DisplayName != "" {
|
||||
name = fmt.Sprintf("%s (%s)", m.ID, m.DisplayName)
|
||||
}
|
||||
fmt.Printf(" %s %s\n", status, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authLoginOpenAI(useDeviceCode bool) {
|
||||
|
|
@ -121,10 +269,8 @@ func authLoginOpenAI(useDeviceCode bool) {
|
|||
|
||||
appCfg, err := loadConfig()
|
||||
if err == nil {
|
||||
// Update Providers (legacy format)
|
||||
appCfg.Providers.OpenAI.AuthMethod = "oauth"
|
||||
|
||||
// Update or add openai in ModelList
|
||||
foundOpenAI := false
|
||||
for i := range appCfg.ModelList {
|
||||
if isOpenAIModel(appCfg.ModelList[i].Model) {
|
||||
|
|
@ -134,7 +280,6 @@ func authLoginOpenAI(useDeviceCode bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// If no openai in ModelList, add it
|
||||
if !foundOpenAI {
|
||||
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
|
||||
ModelName: "gpt-5.2",
|
||||
|
|
@ -143,7 +288,6 @@ func authLoginOpenAI(useDeviceCode bool) {
|
|||
})
|
||||
}
|
||||
|
||||
// Update default model to use OpenAI
|
||||
appCfg.Agents.Defaults.Model = "gpt-5.2"
|
||||
|
||||
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
||||
|
|
@ -169,7 +313,6 @@ func authLoginGoogleAntigravity() {
|
|||
|
||||
cred.Provider = "google-antigravity"
|
||||
|
||||
// Fetch user email from Google userinfo
|
||||
email, err := fetchGoogleUserEmail(cred.AccessToken)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: could not fetch email: %v\n", err)
|
||||
|
|
@ -178,7 +321,6 @@ func authLoginGoogleAntigravity() {
|
|||
fmt.Printf("Email: %s\n", email)
|
||||
}
|
||||
|
||||
// Fetch Cloud Code Assist project ID
|
||||
projectID, err := providers.FetchAntigravityProjectID(cred.AccessToken)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: could not fetch project ID: %v\n", err)
|
||||
|
|
@ -195,10 +337,8 @@ func authLoginGoogleAntigravity() {
|
|||
|
||||
appCfg, err := loadConfig()
|
||||
if err == nil {
|
||||
// Update Providers (legacy format, for backward compatibility)
|
||||
appCfg.Providers.Antigravity.AuthMethod = "oauth"
|
||||
|
||||
// Update or add antigravity in ModelList
|
||||
foundAntigravity := false
|
||||
for i := range appCfg.ModelList {
|
||||
if isAntigravityModel(appCfg.ModelList[i].Model) {
|
||||
|
|
@ -208,7 +348,6 @@ func authLoginGoogleAntigravity() {
|
|||
}
|
||||
}
|
||||
|
||||
// If no antigravity in ModelList, add it
|
||||
if !foundAntigravity {
|
||||
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
|
||||
ModelName: "gemini-flash",
|
||||
|
|
@ -217,7 +356,6 @@ func authLoginGoogleAntigravity() {
|
|||
})
|
||||
}
|
||||
|
||||
// Update default model
|
||||
appCfg.Agents.Defaults.Model = "gemini-flash"
|
||||
|
||||
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
||||
|
|
@ -225,7 +363,7 @@ func authLoginGoogleAntigravity() {
|
|||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n✓ Google Antigravity login successful!")
|
||||
fmt.Println("\n\u2713 Google Antigravity login successful!")
|
||||
fmt.Println("Default model set to: gemini-flash")
|
||||
fmt.Println("Try it: picoclaw agent -m \"Hello world\"")
|
||||
}
|
||||
|
|
@ -275,7 +413,6 @@ func authLoginPasteToken(provider string) {
|
|||
switch provider {
|
||||
case "anthropic":
|
||||
appCfg.Providers.Anthropic.AuthMethod = "token"
|
||||
// Update ModelList
|
||||
found := false
|
||||
for i := range appCfg.ModelList {
|
||||
if isAnthropicModel(appCfg.ModelList[i].Model) {
|
||||
|
|
@ -291,11 +428,9 @@ func authLoginPasteToken(provider string) {
|
|||
AuthMethod: "token",
|
||||
})
|
||||
}
|
||||
// Update default model
|
||||
appCfg.Agents.Defaults.Model = "claude-sonnet-4.6"
|
||||
case "openai":
|
||||
appCfg.Providers.OpenAI.AuthMethod = "token"
|
||||
// Update ModelList
|
||||
found := false
|
||||
for i := range appCfg.ModelList {
|
||||
if isOpenAIModel(appCfg.ModelList[i].Model) {
|
||||
|
|
@ -311,7 +446,6 @@ func authLoginPasteToken(provider string) {
|
|||
AuthMethod: "token",
|
||||
})
|
||||
}
|
||||
// Update default model
|
||||
appCfg.Agents.Defaults.Model = "gpt-5.2"
|
||||
}
|
||||
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
||||
|
|
@ -323,174 +457,6 @@ func authLoginPasteToken(provider string) {
|
|||
fmt.Printf("Default model set to: %s\n", appCfg.Agents.Defaults.Model)
|
||||
}
|
||||
|
||||
func authLogoutCmd() {
|
||||
provider := ""
|
||||
|
||||
args := os.Args[3:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--provider", "-p":
|
||||
if i+1 < len(args) {
|
||||
provider = args[i+1]
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if provider != "" {
|
||||
if err := auth.DeleteCredential(provider); err != nil {
|
||||
fmt.Printf("Failed to remove credentials: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appCfg, err := loadConfig()
|
||||
if err == nil {
|
||||
// Clear AuthMethod in ModelList
|
||||
for i := range appCfg.ModelList {
|
||||
switch provider {
|
||||
case "openai":
|
||||
if isOpenAIModel(appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
case "anthropic":
|
||||
if isAnthropicModel(appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
case "google-antigravity", "antigravity":
|
||||
if isAntigravityModel(appCfg.ModelList[i].Model) {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear AuthMethod in Providers (legacy)
|
||||
switch provider {
|
||||
case "openai":
|
||||
appCfg.Providers.OpenAI.AuthMethod = ""
|
||||
case "anthropic":
|
||||
appCfg.Providers.Anthropic.AuthMethod = ""
|
||||
case "google-antigravity", "antigravity":
|
||||
appCfg.Providers.Antigravity.AuthMethod = ""
|
||||
}
|
||||
config.SaveConfig(getConfigPath(), appCfg)
|
||||
}
|
||||
|
||||
fmt.Printf("Logged out from %s\n", provider)
|
||||
} 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 {
|
||||
// Clear all AuthMethods in ModelList
|
||||
for i := range appCfg.ModelList {
|
||||
appCfg.ModelList[i].AuthMethod = ""
|
||||
}
|
||||
// Clear all AuthMethods in Providers (legacy)
|
||||
appCfg.Providers.OpenAI.AuthMethod = ""
|
||||
appCfg.Providers.Anthropic.AuthMethod = ""
|
||||
appCfg.Providers.Antigravity.AuthMethod = ""
|
||||
config.SaveConfig(getConfigPath(), appCfg)
|
||||
}
|
||||
|
||||
fmt.Println("Logged out from all providers")
|
||||
}
|
||||
}
|
||||
|
||||
func authStatusCmd() {
|
||||
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.Email != "" {
|
||||
fmt.Printf(" Email: %s\n", cred.Email)
|
||||
}
|
||||
if cred.ProjectID != "" {
|
||||
fmt.Printf(" Project: %s\n", cred.ProjectID)
|
||||
}
|
||||
if !cred.ExpiresAt.IsZero() {
|
||||
fmt.Printf(" Expires: %s\n", cred.ExpiresAt.Format("2006-01-02 15:04"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func authModelsCmd() {
|
||||
cred, err := auth.GetCredential("google-antigravity")
|
||||
if err != nil || cred == nil {
|
||||
fmt.Println("Not logged in to Google Antigravity.")
|
||||
fmt.Println("Run: picoclaw auth login --provider google-antigravity")
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh token if needed
|
||||
if cred.NeedsRefresh() && cred.RefreshToken != "" {
|
||||
oauthCfg := auth.GoogleAntigravityOAuthConfig()
|
||||
refreshed, refreshErr := auth.RefreshAccessToken(cred, oauthCfg)
|
||||
if refreshErr == nil {
|
||||
cred = refreshed
|
||||
_ = auth.SetCredential("google-antigravity", cred)
|
||||
}
|
||||
}
|
||||
|
||||
projectID := cred.ProjectID
|
||||
if projectID == "" {
|
||||
fmt.Println("No project ID stored. Try logging in again.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Fetching models for project: %s\n\n", projectID)
|
||||
|
||||
models, err := providers.FetchAntigravityModels(cred.AccessToken, projectID)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching models: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(models) == 0 {
|
||||
fmt.Println("No models available.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Available Antigravity Models:")
|
||||
fmt.Println("-----------------------------")
|
||||
for _, m := range models {
|
||||
status := "✓"
|
||||
if m.IsExhausted {
|
||||
status = "✗ (quota exhausted)"
|
||||
}
|
||||
name := m.ID
|
||||
if m.DisplayName != "" {
|
||||
name = fmt.Sprintf("%s (%s)", m.ID, m.DisplayName)
|
||||
}
|
||||
fmt.Printf(" %s %s\n", status, name)
|
||||
}
|
||||
}
|
||||
|
||||
// isAntigravityModel checks if a model string belongs to antigravity provider
|
||||
func isAntigravityModel(model string) bool {
|
||||
return model == "antigravity" ||
|
||||
|
|
|
|||
|
|
@ -5,76 +5,102 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/cron"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func cronCmd() {
|
||||
if len(os.Args) < 3 {
|
||||
cronHelp()
|
||||
return
|
||||
func newCronCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cron",
|
||||
Short: "Manage scheduled tasks",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCronListCmd(),
|
||||
newCronAddCmd(),
|
||||
newCronRemoveCmd(),
|
||||
newCronEnableCmd(),
|
||||
newCronDisableCmd(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
subcommand := os.Args[2]
|
||||
func newCronListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all scheduled jobs",
|
||||
RunE: runCronList,
|
||||
}
|
||||
}
|
||||
|
||||
// Load config to get workspace path
|
||||
func newCronAddCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add a new scheduled job",
|
||||
RunE: runCronAdd,
|
||||
}
|
||||
cmd.Flags().StringP("name", "n", "", "Job name")
|
||||
cmd.MarkFlagRequired("name")
|
||||
cmd.Flags().StringP("message", "m", "", "Message for agent")
|
||||
cmd.MarkFlagRequired("message")
|
||||
cmd.Flags().Int64P("every", "e", 0, "Run every N seconds")
|
||||
cmd.Flags().StringP("cron", "c", "", "Cron expression (e.g. '0 9 * * *')")
|
||||
cmd.Flags().BoolP("deliver", "d", false, "Deliver response to channel")
|
||||
cmd.Flags().String("to", "", "Recipient for delivery")
|
||||
cmd.Flags().String("channel", "", "Channel for delivery")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCronRemoveCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "remove <job_id>",
|
||||
Short: "Remove a job by ID",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runCronRemove,
|
||||
}
|
||||
}
|
||||
|
||||
func newCronEnableCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "enable <job_id>",
|
||||
Short: "Enable a job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runCronEnable,
|
||||
}
|
||||
}
|
||||
|
||||
func newCronDisableCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "disable <job_id>",
|
||||
Short: "Disable a job",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runCronDisable,
|
||||
}
|
||||
}
|
||||
|
||||
func getCronStorePath() (string, error) {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
cronStorePath := filepath.Join(cfg.WorkspacePath(), "cron", "jobs.json")
|
||||
|
||||
switch subcommand {
|
||||
case "list":
|
||||
cronListCmd(cronStorePath)
|
||||
case "add":
|
||||
cronAddCmd(cronStorePath)
|
||||
case "remove":
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: picoclaw cron remove <job_id>")
|
||||
return
|
||||
}
|
||||
cronRemoveCmd(cronStorePath, os.Args[3])
|
||||
case "enable":
|
||||
cronEnableCmd(cronStorePath, false)
|
||||
case "disable":
|
||||
cronEnableCmd(cronStorePath, true)
|
||||
default:
|
||||
fmt.Printf("Unknown cron command: %s\n", subcommand)
|
||||
cronHelp()
|
||||
return "", fmt.Errorf("Error loading config: %w", err)
|
||||
}
|
||||
return filepath.Join(cfg.WorkspacePath(), "cron", "jobs.json"), nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
func runCronList(cmd *cobra.Command, args []string) error {
|
||||
storePath, err := getCronStorePath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cronListCmd(storePath string) {
|
||||
cs := cron.NewCronService(storePath, nil)
|
||||
jobs := cs.ListJobs(true) // Show all jobs, including disabled
|
||||
jobs := cs.ListJobs(true)
|
||||
|
||||
if len(jobs) == 0 {
|
||||
fmt.Println("No scheduled jobs.")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("\nScheduled Jobs:")
|
||||
|
|
@ -105,75 +131,32 @@ func cronListCmd(storePath string) {
|
|||
fmt.Printf(" Status: %s\n", status)
|
||||
fmt.Printf(" Next run: %s\n", nextRun)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cronAddCmd(storePath string) {
|
||||
name := ""
|
||||
message := ""
|
||||
var everySec *int64
|
||||
cronExpr := ""
|
||||
deliver := false
|
||||
channel := ""
|
||||
to := ""
|
||||
|
||||
args := os.Args[3:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "-n", "--name":
|
||||
if i+1 < len(args) {
|
||||
name = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "-m", "--message":
|
||||
if i+1 < len(args) {
|
||||
message = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "-e", "--every":
|
||||
if i+1 < len(args) {
|
||||
var sec int64
|
||||
fmt.Sscanf(args[i+1], "%d", &sec)
|
||||
everySec = &sec
|
||||
i++
|
||||
}
|
||||
case "-c", "--cron":
|
||||
if i+1 < len(args) {
|
||||
cronExpr = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "-d", "--deliver":
|
||||
deliver = true
|
||||
case "--to":
|
||||
if i+1 < len(args) {
|
||||
to = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--channel":
|
||||
if i+1 < len(args) {
|
||||
channel = args[i+1]
|
||||
i++
|
||||
}
|
||||
}
|
||||
func runCronAdd(cmd *cobra.Command, args []string) error {
|
||||
storePath, err := getCronStorePath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
fmt.Println("Error: --name is required")
|
||||
return
|
||||
}
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
message, _ := cmd.Flags().GetString("message")
|
||||
everySec, _ := cmd.Flags().GetInt64("every")
|
||||
cronExpr, _ := cmd.Flags().GetString("cron")
|
||||
deliver, _ := cmd.Flags().GetBool("deliver")
|
||||
to, _ := cmd.Flags().GetString("to")
|
||||
channel, _ := cmd.Flags().GetString("channel")
|
||||
|
||||
if message == "" {
|
||||
fmt.Println("Error: --message is required")
|
||||
return
|
||||
}
|
||||
|
||||
if everySec == nil && cronExpr == "" {
|
||||
if everySec == 0 && cronExpr == "" {
|
||||
fmt.Println("Error: Either --every or --cron must be specified")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var schedule cron.CronSchedule
|
||||
if everySec != nil {
|
||||
everyMS := *everySec * 1000
|
||||
if everySec != 0 {
|
||||
everyMS := everySec * 1000
|
||||
schedule = cron.CronSchedule{
|
||||
Kind: "every",
|
||||
EveryMS: &everyMS,
|
||||
|
|
@ -189,39 +172,62 @@ func cronAddCmd(storePath string) {
|
|||
job, err := cs.AddJob(name, schedule, message, deliver, channel, to)
|
||||
if err != nil {
|
||||
fmt.Printf("Error adding job: %v\n", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID)
|
||||
fmt.Printf("\u2713 Added job '%s' (%s)\n", job.Name, job.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cronRemoveCmd(storePath, jobID string) {
|
||||
func runCronRemove(cmd *cobra.Command, args []string) error {
|
||||
storePath, err := getCronStorePath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
jobID := args[0]
|
||||
cs := cron.NewCronService(storePath, nil)
|
||||
if cs.RemoveJob(jobID) {
|
||||
fmt.Printf("✓ Removed job %s\n", jobID)
|
||||
fmt.Printf("\u2713 Removed job %s\n", jobID)
|
||||
} else {
|
||||
fmt.Printf("✗ Job %s not found\n", jobID)
|
||||
fmt.Printf("\u2717 Job %s not found\n", jobID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cronEnableCmd(storePath string, disable bool) {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: picoclaw cron enable/disable <job_id>")
|
||||
return
|
||||
func runCronEnable(cmd *cobra.Command, args []string) error {
|
||||
storePath, err := getCronStorePath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
jobID := os.Args[3]
|
||||
jobID := args[0]
|
||||
cs := cron.NewCronService(storePath, nil)
|
||||
enabled := !disable
|
||||
|
||||
job := cs.EnableJob(jobID, enabled)
|
||||
job := cs.EnableJob(jobID, true)
|
||||
if job != nil {
|
||||
status := "enabled"
|
||||
if disable {
|
||||
status = "disabled"
|
||||
}
|
||||
fmt.Printf("✓ Job '%s' %s\n", job.Name, status)
|
||||
fmt.Printf("\u2713 Job '%s' enabled\n", job.Name)
|
||||
} else {
|
||||
fmt.Printf("✗ Job %s not found\n", jobID)
|
||||
fmt.Printf("\u2717 Job %s not found\n", jobID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCronDisable(cmd *cobra.Command, args []string) error {
|
||||
storePath, err := getCronStorePath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
jobID := args[0]
|
||||
cs := cron.NewCronService(storePath, nil)
|
||||
job := cs.EnableJob(jobID, false)
|
||||
if job != nil {
|
||||
fmt.Printf("\u2713 Job '%s' disabled\n", job.Name)
|
||||
} else {
|
||||
fmt.Printf("\u2717 Job %s not found\n", jobID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,24 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/state"
|
||||
"github.com/sipeed/picoclaw/pkg/tools"
|
||||
"github.com/sipeed/picoclaw/pkg/voice"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func gatewayCmd() {
|
||||
// Check for --debug flag
|
||||
args := os.Args[2:]
|
||||
for _, arg := range args {
|
||||
if arg == "--debug" || arg == "-d" {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
break
|
||||
}
|
||||
func newGatewayCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "gateway",
|
||||
Short: "Start picoclaw gateway",
|
||||
RunE: runGateway,
|
||||
}
|
||||
cmd.Flags().BoolP("debug", "d", false, "Enable debug mode")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGateway(cmd *cobra.Command, args []string) error {
|
||||
debug, _ := cmd.Flags().GetBool("debug")
|
||||
if debug {
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("\U0001f50d Debug mode enabled")
|
||||
}
|
||||
|
||||
cfg, err := loadConfig()
|
||||
|
|
@ -49,7 +56,6 @@ func gatewayCmd() {
|
|||
fmt.Printf("Error creating provider: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Use the resolved model ID from provider creation
|
||||
if modelID != "" {
|
||||
cfg.Agents.Defaults.Model = modelID
|
||||
}
|
||||
|
|
@ -57,17 +63,15 @@ func gatewayCmd() {
|
|||
msgBus := bus.NewMessageBus()
|
||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
||||
|
||||
// Print agent startup info
|
||||
fmt.Println("\n📦 Agent Status:")
|
||||
fmt.Println("\n\U0001f4e6 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",
|
||||
fmt.Printf(" \u2022 Tools: %d loaded\n", toolsInfo["count"])
|
||||
fmt.Printf(" \u2022 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"],
|
||||
|
|
@ -75,7 +79,6 @@ func gatewayCmd() {
|
|||
"skills_available": skillsInfo["available"],
|
||||
})
|
||||
|
||||
// Setup cron tool and service
|
||||
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
||||
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg)
|
||||
|
||||
|
|
@ -86,11 +89,9 @@ func gatewayCmd() {
|
|||
)
|
||||
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))
|
||||
|
|
@ -98,8 +99,6 @@ func gatewayCmd() {
|
|||
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)
|
||||
})
|
||||
|
||||
|
|
@ -109,7 +108,6 @@ func gatewayCmd() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Inject channel manager into agent loop for command handling
|
||||
agentLoop.SetChannelManager(channelManager)
|
||||
|
||||
var transcriber *voice.GroqTranscriber
|
||||
|
|
@ -141,12 +139,12 @@ func gatewayCmd() {
|
|||
|
||||
enabledChannels := channelManager.GetEnabledChannels()
|
||||
if len(enabledChannels) > 0 {
|
||||
fmt.Printf("✓ Channels enabled: %s\n", enabledChannels)
|
||||
fmt.Printf("\u2713 Channels enabled: %s\n", enabledChannels)
|
||||
} else {
|
||||
fmt.Println("⚠ Warning: No channels enabled")
|
||||
fmt.Println("\u26a0 Warning: No channels enabled")
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
fmt.Printf("\u2713 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())
|
||||
|
|
@ -155,12 +153,12 @@ func gatewayCmd() {
|
|||
if err := cronService.Start(); err != nil {
|
||||
fmt.Printf("Error starting cron service: %v\n", err)
|
||||
}
|
||||
fmt.Println("✓ Cron service started")
|
||||
fmt.Println("\u2713 Cron service started")
|
||||
|
||||
if err := heartbeatService.Start(); err != nil {
|
||||
fmt.Printf("Error starting heartbeat service: %v\n", err)
|
||||
}
|
||||
fmt.Println("✓ Heartbeat service started")
|
||||
fmt.Println("\u2713 Heartbeat service started")
|
||||
|
||||
stateManager := state.NewManager(cfg.WorkspacePath())
|
||||
deviceService := devices.NewService(devices.Config{
|
||||
|
|
@ -171,7 +169,7 @@ func gatewayCmd() {
|
|||
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")
|
||||
fmt.Println("\u2713 Device event service started")
|
||||
}
|
||||
|
||||
if err := channelManager.StartAll(ctx); err != nil {
|
||||
|
|
@ -184,7 +182,7 @@ func gatewayCmd() {
|
|||
logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
}()
|
||||
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
fmt.Printf("\u2713 Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
|
||||
go agentLoop.Run(ctx)
|
||||
|
||||
|
|
@ -200,20 +198,18 @@ func gatewayCmd() {
|
|||
cronService.Stop()
|
||||
agentLoop.Stop()
|
||||
channelManager.StopAll(ctx)
|
||||
fmt.Println("✓ Gateway stopped")
|
||||
fmt.Println("\u2713 Gateway stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, cfg *config.Config) *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, restrict, execTimeout, cfg)
|
||||
agentLoop.RegisterTool(cronTool)
|
||||
|
||||
// Set the onJob handler
|
||||
cronService.SetOnJob(func(job *cron.CronJob) (string, error) {
|
||||
result := cronTool.ExecuteJob(context.Background(), job)
|
||||
return result, nil
|
||||
|
|
|
|||
|
|
@ -8,45 +8,41 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/migrate"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func migrateCmd() {
|
||||
if len(os.Args) > 2 && (os.Args[2] == "--help" || os.Args[2] == "-h") {
|
||||
migrateHelp()
|
||||
return
|
||||
}
|
||||
func newMigrateCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Migrate from OpenClaw to PicoClaw",
|
||||
Long: `Migrate from OpenClaw to PicoClaw
|
||||
|
||||
Examples:
|
||||
picoclaw migrate
|
||||
picoclaw migrate --dry-run
|
||||
picoclaw migrate --refresh
|
||||
picoclaw migrate --force`,
|
||||
RunE: runMigrate,
|
||||
}
|
||||
cmd.Flags().Bool("dry-run", false, "Show what would be migrated without making changes")
|
||||
cmd.Flags().Bool("config-only", false, "Only migrate config, skip workspace files")
|
||||
cmd.Flags().Bool("workspace-only", false, "Only migrate workspace files, skip config")
|
||||
cmd.Flags().Bool("force", false, "Skip confirmation prompts")
|
||||
cmd.Flags().Bool("refresh", false, "Re-sync workspace files from OpenClaw (repeatable)")
|
||||
cmd.Flags().String("openclaw-home", "", "Override OpenClaw home directory (default: ~/.openclaw)")
|
||||
cmd.Flags().String("picoclaw-home", "", "Override PicoClaw home directory (default: ~/.picoclaw)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runMigrate(cmd *cobra.Command, args []string) error {
|
||||
opts := migrate.Options{}
|
||||
|
||||
args := os.Args[2:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--dry-run":
|
||||
opts.DryRun = true
|
||||
case "--config-only":
|
||||
opts.ConfigOnly = true
|
||||
case "--workspace-only":
|
||||
opts.WorkspaceOnly = true
|
||||
case "--force":
|
||||
opts.Force = true
|
||||
case "--refresh":
|
||||
opts.Refresh = true
|
||||
case "--openclaw-home":
|
||||
if i+1 < len(args) {
|
||||
opts.OpenClawHome = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--picoclaw-home":
|
||||
if i+1 < len(args) {
|
||||
opts.PicoClawHome = args[i+1]
|
||||
i++
|
||||
}
|
||||
default:
|
||||
fmt.Printf("Unknown flag: %s\n", args[i])
|
||||
migrateHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
opts.DryRun, _ = cmd.Flags().GetBool("dry-run")
|
||||
opts.ConfigOnly, _ = cmd.Flags().GetBool("config-only")
|
||||
opts.WorkspaceOnly, _ = cmd.Flags().GetBool("workspace-only")
|
||||
opts.Force, _ = cmd.Flags().GetBool("force")
|
||||
opts.Refresh, _ = cmd.Flags().GetBool("refresh")
|
||||
opts.OpenClawHome, _ = cmd.Flags().GetString("openclaw-home")
|
||||
opts.PicoClawHome, _ = cmd.Flags().GetString("picoclaw-home")
|
||||
|
||||
result, err := migrate.Run(opts)
|
||||
if err != nil {
|
||||
|
|
@ -57,25 +53,5 @@ func migrateCmd() {
|
|||
if !opts.DryRun {
|
||||
migrate.PrintSummary(result)
|
||||
}
|
||||
}
|
||||
|
||||
func migrateHelp() {
|
||||
fmt.Println("\nMigrate from OpenClaw to PicoClaw")
|
||||
fmt.Println()
|
||||
fmt.Println("Usage: picoclaw migrate [options]")
|
||||
fmt.Println()
|
||||
fmt.Println("Options:")
|
||||
fmt.Println(" --dry-run Show what would be migrated without making changes")
|
||||
fmt.Println(" --refresh Re-sync workspace files from OpenClaw (repeatable)")
|
||||
fmt.Println(" --config-only Only migrate config, skip workspace files")
|
||||
fmt.Println(" --workspace-only Only migrate workspace files, skip config")
|
||||
fmt.Println(" --force Skip confirmation prompts")
|
||||
fmt.Println(" --openclaw-home Override OpenClaw home directory (default: ~/.openclaw)")
|
||||
fmt.Println(" --picoclaw-home Override PicoClaw home directory (default: ~/.picoclaw)")
|
||||
fmt.Println()
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" picoclaw migrate Detect and migrate from OpenClaw")
|
||||
fmt.Println(" picoclaw migrate --dry-run Show what would be migrated")
|
||||
fmt.Println(" picoclaw migrate --refresh Re-sync workspace files")
|
||||
fmt.Println(" picoclaw migrate --force Migrate without confirmation")
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,22 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//go:generate cp -r ../../workspace .
|
||||
//go:embed workspace
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
func onboard() {
|
||||
func newOnboardCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "onboard",
|
||||
Short: "Initialize picoclaw configuration and workspace",
|
||||
RunE: runOnboard,
|
||||
}
|
||||
}
|
||||
|
||||
func runOnboard(cmd *cobra.Command, args []string) error {
|
||||
configPath := getConfigPath()
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
|
|
@ -27,7 +36,7 @@ func onboard() {
|
|||
fmt.Scanln(&response)
|
||||
if response != "y" {
|
||||
fmt.Println("Aborted.")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,26 +60,23 @@ func onboard() {
|
|||
fmt.Println(" See README.md for 17+ supported providers.")
|
||||
fmt.Println("")
|
||||
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -81,15 +87,12 @@ func copyEmbeddedToTarget(targetDir string) error {
|
|||
return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err)
|
||||
}
|
||||
|
||||
// Build target file path
|
||||
targetPath := filepath.Join(targetDir, new_path)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,33 +14,129 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
"github.com/sipeed/picoclaw/pkg/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
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")
|
||||
fmt.Println(" picoclaw skills install --registry clawhub github")
|
||||
type skillsContext struct {
|
||||
installer *skills.SkillInstaller
|
||||
loader *skills.SkillsLoader
|
||||
workspace string
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func skillsListCmd(loader *skills.SkillsLoader) {
|
||||
allSkills := loader.ListSkills()
|
||||
func loadSkillsContext() (*skillsContext, error) {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error loading config: %w", err)
|
||||
}
|
||||
|
||||
workspace := cfg.WorkspacePath()
|
||||
installer := skills.NewSkillInstaller(workspace)
|
||||
globalDir := filepath.Dir(getConfigPath())
|
||||
globalSkillsDir := filepath.Join(globalDir, "skills")
|
||||
builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills")
|
||||
loader := skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir)
|
||||
|
||||
return &skillsContext{
|
||||
installer: installer,
|
||||
loader: loader,
|
||||
workspace: workspace,
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newSkillsCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "skills",
|
||||
Short: "Manage skills (install, list, remove)",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newSkillsListCmd(),
|
||||
newSkillsInstallCmd(),
|
||||
newSkillsRemoveCmd(),
|
||||
newSkillsInstallBuiltinCmd(),
|
||||
newSkillsListBuiltinCmd(),
|
||||
newSkillsSearchCmd(),
|
||||
newSkillsShowCmd(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newSkillsListCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List installed skills",
|
||||
RunE: runSkillsList,
|
||||
}
|
||||
}
|
||||
|
||||
func newSkillsInstallCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "install <repo>",
|
||||
Short: "Install skill from GitHub",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: runSkillsInstall,
|
||||
}
|
||||
// Add --registry flag support
|
||||
cmd.Flags().String("registry", "", "Install from registry (e.g., clawhub)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newSkillsRemoveCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "remove <name>",
|
||||
Aliases: []string{"uninstall"},
|
||||
Short: "Remove installed skill",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runSkillsRemove,
|
||||
}
|
||||
}
|
||||
|
||||
func newSkillsInstallBuiltinCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "install-builtin",
|
||||
Short: "Install all builtin skills to workspace",
|
||||
RunE: runSkillsInstallBuiltin,
|
||||
}
|
||||
}
|
||||
|
||||
func newSkillsListBuiltinCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list-builtin",
|
||||
Short: "List available builtin skills",
|
||||
RunE: runSkillsListBuiltin,
|
||||
}
|
||||
}
|
||||
|
||||
func newSkillsSearchCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "search",
|
||||
Short: "Search available skills",
|
||||
RunE: runSkillsSearch,
|
||||
}
|
||||
}
|
||||
|
||||
func newSkillsShowCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "show <name>",
|
||||
Short: "Show skill details",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runSkillsShow,
|
||||
}
|
||||
}
|
||||
|
||||
func runSkillsList(cmd *cobra.Command, args []string) error {
|
||||
sc, err := loadSkillsContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
allSkills := sc.loader.ListSkills()
|
||||
|
||||
if len(allSkills) == 0 {
|
||||
fmt.Println("No skills installed.")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("\nInstalled Skills:")
|
||||
|
|
@ -51,54 +147,55 @@ func skillsListCmd(loader *skills.SkillsLoader) {
|
|||
fmt.Printf(" %s\n", skill.Description)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func skillsInstallCmd(installer *skills.SkillInstaller, cfg *config.Config) {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: picoclaw skills install <github-repo>")
|
||||
fmt.Println(" picoclaw skills install --registry <name> <slug>")
|
||||
return
|
||||
func runSkillsInstall(cmd *cobra.Command, args []string) error {
|
||||
sc, err := loadSkillsContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for --registry flag.
|
||||
if os.Args[3] == "--registry" {
|
||||
if len(os.Args) < 6 {
|
||||
// Check for --registry flag
|
||||
registry, _ := cmd.Flags().GetString("registry")
|
||||
if registry != "" {
|
||||
if len(args) < 2 {
|
||||
fmt.Println("Usage: picoclaw skills install --registry <name> <slug>")
|
||||
fmt.Println("Example: picoclaw skills install --registry clawhub github")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
registryName := os.Args[4]
|
||||
slug := os.Args[5]
|
||||
skillsInstallFromRegistry(cfg, registryName, slug)
|
||||
return
|
||||
slug := args[1]
|
||||
return skillsInstallFromRegistry(sc.cfg, registry, slug)
|
||||
}
|
||||
|
||||
// Default: install from GitHub (backward compatible).
|
||||
repo := os.Args[3]
|
||||
// Default: install from GitHub
|
||||
repo := args[0]
|
||||
fmt.Printf("Installing skill from %s...\n", repo)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := installer.InstallFromGitHub(ctx, repo); err != nil {
|
||||
fmt.Printf("\u2717 Failed to install skill: %v\n", err)
|
||||
if err := sc.installer.InstallFromGitHub(ctx, repo); err != nil {
|
||||
fmt.Printf("✗ Failed to install skill: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("\u2713 Skill '%s' installed successfully!\n", filepath.Base(repo))
|
||||
fmt.Printf("✓ Skill '%s' installed successfully!\n", filepath.Base(repo))
|
||||
return nil
|
||||
}
|
||||
|
||||
// skillsInstallFromRegistry installs a skill from a named registry (e.g. clawhub).
|
||||
func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
|
||||
func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) error {
|
||||
err := utils.ValidateSkillIdentifier(registryName)
|
||||
if err != nil {
|
||||
fmt.Printf("\u2717 Invalid registry name: %v\n", err)
|
||||
fmt.Printf("✗ Invalid registry name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = utils.ValidateSkillIdentifier(slug)
|
||||
if err != nil {
|
||||
fmt.Printf("\u2717 Invalid slug: %v\n", err)
|
||||
fmt.Printf("✗ Invalid slug: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +208,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
|
|||
|
||||
registry := registryMgr.GetRegistry(registryName)
|
||||
if registry == nil {
|
||||
fmt.Printf("\u2717 Registry '%s' not found or not enabled. Check your config.json.\n", registryName)
|
||||
fmt.Printf("✗ Registry '%s' not found or not enabled. Check your config.json.\n", registryName)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +216,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
|
|||
targetDir := filepath.Join(workspace, "skills", slug)
|
||||
|
||||
if _, err := os.Stat(targetDir); err == nil {
|
||||
fmt.Printf("\u2717 Skill '%s' already installed at %s\n", slug, targetDir)
|
||||
fmt.Printf("✗ Skill '%s' already installed at %s\n", slug, targetDir)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +224,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
|
|||
defer cancel()
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(workspace, "skills"), 0755); err != nil {
|
||||
fmt.Printf("\u2717 Failed to create skills directory: %v\n", err)
|
||||
fmt.Printf("✗ Failed to create skills directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -135,45 +232,60 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
|
|||
if err != nil {
|
||||
rmErr := os.RemoveAll(targetDir)
|
||||
if rmErr != nil {
|
||||
fmt.Printf("\u2717 Failed to remove partial install: %v\n", rmErr)
|
||||
fmt.Printf("✗ Failed to remove partial install: %v\n", rmErr)
|
||||
}
|
||||
fmt.Printf("\u2717 Failed to install skill: %v\n", err)
|
||||
fmt.Printf("✗ Failed to install skill: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if result.IsMalwareBlocked {
|
||||
rmErr := os.RemoveAll(targetDir)
|
||||
if rmErr != nil {
|
||||
fmt.Printf("\u2717 Failed to remove partial install: %v\n", rmErr)
|
||||
fmt.Printf("✗ Failed to remove partial install: %v\n", rmErr)
|
||||
}
|
||||
fmt.Printf("\u2717 Skill '%s' is flagged as malicious and cannot be installed.\n", slug)
|
||||
fmt.Printf("✗ Skill '%s' is flagged as malicious and cannot be installed.\n", slug)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if result.IsSuspicious {
|
||||
fmt.Printf("\u26a0\ufe0f Warning: skill '%s' is flagged as suspicious.\n", slug)
|
||||
fmt.Printf("⚠️ Warning: skill '%s' is flagged as suspicious.\n", slug)
|
||||
}
|
||||
|
||||
fmt.Printf("\u2713 Skill '%s' v%s installed successfully!\n", slug, result.Version)
|
||||
fmt.Printf("✓ Skill '%s' v%s installed successfully!\n", slug, result.Version)
|
||||
if result.Summary != "" {
|
||||
fmt.Printf(" %s\n", result.Summary)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) {
|
||||
func runSkillsRemove(cmd *cobra.Command, args []string) error {
|
||||
sc, err := loadSkillsContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
skillName := args[0]
|
||||
fmt.Printf("Removing skill '%s'...\n", skillName)
|
||||
|
||||
if err := installer.Uninstall(skillName); err != nil {
|
||||
if err := sc.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)
|
||||
return nil
|
||||
}
|
||||
|
||||
func skillsInstallBuiltinCmd(workspace string) {
|
||||
func runSkillsInstallBuiltin(cmd *cobra.Command, args []string) error {
|
||||
sc, err := loadSkillsContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
builtinSkillsDir := "./picoclaw/skills"
|
||||
workspaceSkillsDir := filepath.Join(workspace, "skills")
|
||||
workspaceSkillsDir := filepath.Join(sc.workspace, "skills")
|
||||
|
||||
fmt.Printf("Copying builtin skills to workspace...\n")
|
||||
|
||||
|
|
@ -205,13 +317,14 @@ func skillsInstallBuiltinCmd(workspace string) {
|
|||
|
||||
fmt.Println("\n✓ All builtin skills installed!")
|
||||
fmt.Println("Now you can use them in your workspace.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func skillsListBuiltinCmd() {
|
||||
func runSkillsListBuiltin(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
builtinSkillsDir := filepath.Join(filepath.Dir(cfg.WorkspacePath()), "picoclaw", "skills")
|
||||
|
||||
|
|
@ -221,12 +334,12 @@ func skillsListBuiltinCmd() {
|
|||
entries, err := os.ReadDir(builtinSkillsDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading builtin skills: %v\n", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
fmt.Println("No builtin skills available.")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
|
|
@ -257,23 +370,30 @@ func skillsListBuiltinCmd() {
|
|||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func skillsSearchCmd(installer *skills.SkillInstaller) {
|
||||
func runSkillsSearch(cmd *cobra.Command, args []string) error {
|
||||
sc, err := loadSkillsContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Searching for available skills...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
availableSkills, err := installer.ListAvailableSkills(ctx)
|
||||
availableSkills, err := sc.installer.ListAvailableSkills(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("✗ Failed to fetch skills list: %v\n", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(availableSkills) == 0 {
|
||||
fmt.Println("No skills available.")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nAvailable Skills (%d):\n", len(availableSkills))
|
||||
|
|
@ -290,16 +410,25 @@ func skillsSearchCmd(installer *skills.SkillInstaller) {
|
|||
}
|
||||
fmt.Println()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func skillsShowCmd(loader *skills.SkillsLoader, skillName string) {
|
||||
content, ok := loader.LoadSkill(skillName)
|
||||
func runSkillsShow(cmd *cobra.Command, args []string) error {
|
||||
sc, err := loadSkillsContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
skillName := args[0]
|
||||
content, ok := sc.loader.LoadSkill(skillName)
|
||||
if !ok {
|
||||
fmt.Printf("✗ Skill '%s' not found\n", skillName)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\n📦 Skill: %s\n", skillName)
|
||||
fmt.Println("----------------------")
|
||||
fmt.Println(content)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,22 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/auth"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func statusCmd() {
|
||||
func newStatusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show picoclaw status",
|
||||
RunE: runStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func runStatus(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
configPath := getConfigPath()
|
||||
|
|
@ -28,16 +37,16 @@ func statusCmd() {
|
|||
fmt.Println()
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
fmt.Println("Config:", configPath, "✓")
|
||||
fmt.Println("Config:", configPath, "\u2713")
|
||||
} else {
|
||||
fmt.Println("Config:", configPath, "✗")
|
||||
fmt.Println("Config:", configPath, "\u2717")
|
||||
}
|
||||
|
||||
workspace := cfg.WorkspacePath()
|
||||
if _, err := os.Stat(workspace); err == nil {
|
||||
fmt.Println("Workspace:", workspace, "✓")
|
||||
fmt.Println("Workspace:", workspace, "\u2713")
|
||||
} else {
|
||||
fmt.Println("Workspace:", workspace, "✗")
|
||||
fmt.Println("Workspace:", workspace, "\u2717")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
|
|
@ -59,7 +68,7 @@ func statusCmd() {
|
|||
|
||||
status := func(enabled bool) string {
|
||||
if enabled {
|
||||
return "✓"
|
||||
return "\u2713"
|
||||
}
|
||||
return "not set"
|
||||
}
|
||||
|
|
@ -75,12 +84,12 @@ func statusCmd() {
|
|||
fmt.Println("VolcEngine API:", status(hasVolcEngine))
|
||||
fmt.Println("Nvidia API:", status(hasNvidia))
|
||||
if hasVLLM {
|
||||
fmt.Printf("vLLM/Local: ✓ %s\n", cfg.Providers.VLLM.APIBase)
|
||||
fmt.Printf("vLLM/Local: \u2713 %s\n", cfg.Providers.VLLM.APIBase)
|
||||
} else {
|
||||
fmt.Println("vLLM/Local: not set")
|
||||
}
|
||||
if hasOllama {
|
||||
fmt.Printf("Ollama: ✓ %s\n", cfg.Providers.Ollama.APIBase)
|
||||
fmt.Printf("Ollama: \u2713 %s\n", cfg.Providers.Ollama.APIBase)
|
||||
} else {
|
||||
fmt.Println("Ollama: not set")
|
||||
}
|
||||
|
|
@ -99,4 +108,5 @@ func statusCmd() {
|
|||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ import (
|
|||
"runtime"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
gitCommit string
|
||||
buildTime string
|
||||
goVersion string
|
||||
version = "dev"
|
||||
gitCommit string
|
||||
buildTime string
|
||||
goVersion string
|
||||
)
|
||||
|
||||
const logo = "🦞"
|
||||
|
|
@ -92,101 +92,40 @@ func copyDirectory(src, dst string) error {
|
|||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
printHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "picoclaw",
|
||||
Short: fmt.Sprintf("%s picoclaw - Personal AI Assistant", logo),
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
|
||||
switch command {
|
||||
case "onboard":
|
||||
onboard()
|
||||
case "agent":
|
||||
agentCmd()
|
||||
case "gateway":
|
||||
gatewayCmd()
|
||||
case "status":
|
||||
statusCmd()
|
||||
case "migrate":
|
||||
migrateCmd()
|
||||
case "auth":
|
||||
authCmd()
|
||||
case "cron":
|
||||
cronCmd()
|
||||
case "skills":
|
||||
if len(os.Args) < 3 {
|
||||
skillsHelp()
|
||||
return
|
||||
}
|
||||
|
||||
subcommand := os.Args[2]
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
workspace := cfg.WorkspacePath()
|
||||
installer := skills.NewSkillInstaller(workspace)
|
||||
// 获取全局配置目录和内置 skills 目录
|
||||
globalDir := filepath.Dir(getConfigPath())
|
||||
globalSkillsDir := filepath.Join(globalDir, "skills")
|
||||
builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills")
|
||||
skillsLoader := skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir)
|
||||
|
||||
switch subcommand {
|
||||
case "list":
|
||||
skillsListCmd(skillsLoader)
|
||||
case "install":
|
||||
skillsInstallCmd(installer, cfg)
|
||||
case "remove", "uninstall":
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: picoclaw skills remove <skill-name>")
|
||||
return
|
||||
}
|
||||
skillsRemoveCmd(installer, os.Args[3])
|
||||
case "install-builtin":
|
||||
skillsInstallBuiltinCmd(workspace)
|
||||
case "list-builtin":
|
||||
skillsListBuiltinCmd()
|
||||
case "search":
|
||||
skillsSearchCmd(installer)
|
||||
case "show":
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: picoclaw skills show <skill-name>")
|
||||
return
|
||||
}
|
||||
skillsShowCmd(skillsLoader, os.Args[3])
|
||||
default:
|
||||
fmt.Printf("Unknown skills command: %s\n", subcommand)
|
||||
skillsHelp()
|
||||
}
|
||||
case "version", "--version", "-v":
|
||||
printVersion()
|
||||
default:
|
||||
fmt.Printf("Unknown command: %s\n", command)
|
||||
printHelp()
|
||||
os.Exit(1)
|
||||
func newVersionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
printVersion()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.AddCommand(
|
||||
newOnboardCmd(),
|
||||
newAgentCmd(),
|
||||
newGatewayCmd(),
|
||||
newStatusCmd(),
|
||||
newMigrateCmd(),
|
||||
newAuthCmd(),
|
||||
newCronCmd(),
|
||||
newSkillsCmd(),
|
||||
newVersionCmd(),
|
||||
)
|
||||
rootCmd.Version = formatVersion()
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
|
|
|
|||
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