refactor: parsing cli arguments use cobra

Signed-off-by: Kai Xia <kaix+github@fastmail.com>
This commit is contained in:
Kai Xia 2026-02-20 17:15:22 +11:00
parent 1ef33c90ed
commit 446b4eca23
11 changed files with 697 additions and 665 deletions

View file

@ -17,35 +17,31 @@ import (
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
"github.com/spf13/cobra"
) )
func agentCmd() { func newAgentCmd() *cobra.Command {
message := "" cmd := &cobra.Command{
sessionKey := "cli:default" Use: "agent",
modelOverride := "" 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:] func runAgent(cmd *cobra.Command, args []string) error {
for i := 0; i < len(args); i++ { debug, _ := cmd.Flags().GetBool("debug")
switch args[i] { message, _ := cmd.Flags().GetString("message")
case "--debug", "-d": sessionKey, _ := cmd.Flags().GetString("session")
logger.SetLevel(logger.DEBUG) modelOverride, _ := cmd.Flags().GetString("model")
fmt.Println("🔍 Debug mode enabled")
case "-m", "--message": if debug {
if i+1 < len(args) { logger.SetLevel(logger.DEBUG)
message = args[i+1] fmt.Println("\U0001f50d Debug mode enabled")
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++
}
}
} }
cfg, err := loadConfig() cfg, err := loadConfig()
@ -63,7 +59,6 @@ func agentCmd() {
fmt.Printf("Error creating provider: %v\n", err) fmt.Printf("Error creating provider: %v\n", err)
os.Exit(1) os.Exit(1)
} }
// Use the resolved model ID from provider creation
if modelID != "" { if modelID != "" {
cfg.Agents.Defaults.Model = modelID cfg.Agents.Defaults.Model = modelID
} }
@ -71,7 +66,6 @@ func agentCmd() {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
// Print agent startup info (only for interactive mode)
startupInfo := agentLoop.GetStartupInfo() startupInfo := agentLoop.GetStartupInfo()
logger.InfoCF("agent", "Agent initialized", logger.InfoCF("agent", "Agent initialized",
map[string]interface{}{ map[string]interface{}{
@ -92,6 +86,7 @@ func agentCmd() {
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo) fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
interactiveMode(agentLoop, sessionKey) interactiveMode(agentLoop, sessionKey)
} }
return nil
} }
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {

View file

@ -15,74 +15,66 @@ import (
"github.com/sipeed/picoclaw/pkg/auth" "github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
"github.com/spf13/cobra"
) )
const supportedProvidersMsg = "Supported providers: openai, anthropic, google-antigravity" const supportedProvidersMsg = "Supported providers: openai, anthropic, google-antigravity"
func authCmd() { func newAuthCmd() *cobra.Command {
if len(os.Args) < 3 { cmd := &cobra.Command{
authHelp() Use: "auth",
return Short: "Manage authentication (login, logout, status)",
} }
cmd.AddCommand(
newAuthLoginCmd(),
newAuthLogoutCmd(),
newAuthStatusCmd(),
newAuthModelsCmd(),
)
return cmd
}
switch os.Args[2] { func newAuthLoginCmd() *cobra.Command {
case "login": cmd := &cobra.Command{
authLoginCmd() Use: "login",
case "logout": Short: "Login via OAuth or paste token",
authLogoutCmd() RunE: runAuthLogin,
case "status": }
authStatusCmd() cmd.Flags().StringP("provider", "p", "", "Provider (openai, anthropic, google-antigravity)")
case "models": cmd.MarkFlagRequired("provider")
authModelsCmd() cmd.Flags().Bool("device-code", false, "Use device code flow (for headless environments)")
default: return cmd
fmt.Printf("Unknown auth command: %s\n", os.Args[2]) }
authHelp()
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() { func newAuthModelsCmd() *cobra.Command {
fmt.Println("\nAuth commands:") return &cobra.Command{
fmt.Println(" login Login via OAuth or paste token") Use: "models",
fmt.Println(" logout Remove stored credentials") Short: "List available Antigravity models",
fmt.Println(" status Show current auth status") RunE: runAuthModels,
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 authLoginCmd() { func runAuthLogin(cmd *cobra.Command, args []string) error {
provider := "" provider, _ := cmd.Flags().GetString("provider")
useDeviceCode := false useDeviceCode, _ := cmd.Flags().GetBool("device-code")
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
}
switch provider { switch provider {
case "openai": case "openai":
@ -95,6 +87,162 @@ func authLoginCmd() {
fmt.Printf("Unsupported provider: %s\n", provider) fmt.Printf("Unsupported provider: %s\n", provider)
fmt.Println(supportedProvidersMsg) 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) { func authLoginOpenAI(useDeviceCode bool) {
@ -121,10 +269,8 @@ func authLoginOpenAI(useDeviceCode bool) {
appCfg, err := loadConfig() appCfg, err := loadConfig()
if err == nil { if err == nil {
// Update Providers (legacy format)
appCfg.Providers.OpenAI.AuthMethod = "oauth" appCfg.Providers.OpenAI.AuthMethod = "oauth"
// Update or add openai in ModelList
foundOpenAI := false foundOpenAI := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isOpenAIModel(appCfg.ModelList[i].Model) { if isOpenAIModel(appCfg.ModelList[i].Model) {
@ -134,7 +280,6 @@ func authLoginOpenAI(useDeviceCode bool) {
} }
} }
// If no openai in ModelList, add it
if !foundOpenAI { if !foundOpenAI {
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{ appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
ModelName: "gpt-5.2", 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" appCfg.Agents.Defaults.Model = "gpt-5.2"
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil { if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
@ -169,7 +313,6 @@ func authLoginGoogleAntigravity() {
cred.Provider = "google-antigravity" cred.Provider = "google-antigravity"
// Fetch user email from Google userinfo
email, err := fetchGoogleUserEmail(cred.AccessToken) email, err := fetchGoogleUserEmail(cred.AccessToken)
if err != nil { if err != nil {
fmt.Printf("Warning: could not fetch email: %v\n", err) fmt.Printf("Warning: could not fetch email: %v\n", err)
@ -178,7 +321,6 @@ func authLoginGoogleAntigravity() {
fmt.Printf("Email: %s\n", email) fmt.Printf("Email: %s\n", email)
} }
// Fetch Cloud Code Assist project ID
projectID, err := providers.FetchAntigravityProjectID(cred.AccessToken) projectID, err := providers.FetchAntigravityProjectID(cred.AccessToken)
if err != nil { if err != nil {
fmt.Printf("Warning: could not fetch project ID: %v\n", err) fmt.Printf("Warning: could not fetch project ID: %v\n", err)
@ -195,10 +337,8 @@ func authLoginGoogleAntigravity() {
appCfg, err := loadConfig() appCfg, err := loadConfig()
if err == nil { if err == nil {
// Update Providers (legacy format, for backward compatibility)
appCfg.Providers.Antigravity.AuthMethod = "oauth" appCfg.Providers.Antigravity.AuthMethod = "oauth"
// Update or add antigravity in ModelList
foundAntigravity := false foundAntigravity := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isAntigravityModel(appCfg.ModelList[i].Model) { if isAntigravityModel(appCfg.ModelList[i].Model) {
@ -208,7 +348,6 @@ func authLoginGoogleAntigravity() {
} }
} }
// If no antigravity in ModelList, add it
if !foundAntigravity { if !foundAntigravity {
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{ appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
ModelName: "gemini-flash", ModelName: "gemini-flash",
@ -217,7 +356,6 @@ func authLoginGoogleAntigravity() {
}) })
} }
// Update default model
appCfg.Agents.Defaults.Model = "gemini-flash" appCfg.Agents.Defaults.Model = "gemini-flash"
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil { 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("Default model set to: gemini-flash")
fmt.Println("Try it: picoclaw agent -m \"Hello world\"") fmt.Println("Try it: picoclaw agent -m \"Hello world\"")
} }
@ -275,7 +413,6 @@ func authLoginPasteToken(provider string) {
switch provider { switch provider {
case "anthropic": case "anthropic":
appCfg.Providers.Anthropic.AuthMethod = "token" appCfg.Providers.Anthropic.AuthMethod = "token"
// Update ModelList
found := false found := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isAnthropicModel(appCfg.ModelList[i].Model) { if isAnthropicModel(appCfg.ModelList[i].Model) {
@ -291,11 +428,9 @@ func authLoginPasteToken(provider string) {
AuthMethod: "token", AuthMethod: "token",
}) })
} }
// Update default model
appCfg.Agents.Defaults.Model = "claude-sonnet-4.6" appCfg.Agents.Defaults.Model = "claude-sonnet-4.6"
case "openai": case "openai":
appCfg.Providers.OpenAI.AuthMethod = "token" appCfg.Providers.OpenAI.AuthMethod = "token"
// Update ModelList
found := false found := false
for i := range appCfg.ModelList { for i := range appCfg.ModelList {
if isOpenAIModel(appCfg.ModelList[i].Model) { if isOpenAIModel(appCfg.ModelList[i].Model) {
@ -311,7 +446,6 @@ func authLoginPasteToken(provider string) {
AuthMethod: "token", AuthMethod: "token",
}) })
} }
// Update default model
appCfg.Agents.Defaults.Model = "gpt-5.2" appCfg.Agents.Defaults.Model = "gpt-5.2"
} }
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil { 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) 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 // isAntigravityModel checks if a model string belongs to antigravity provider
func isAntigravityModel(model string) bool { func isAntigravityModel(model string) bool {
return model == "antigravity" || return model == "antigravity" ||

View file

@ -5,76 +5,102 @@ package main
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"time" "time"
"github.com/sipeed/picoclaw/pkg/cron" "github.com/sipeed/picoclaw/pkg/cron"
"github.com/spf13/cobra"
) )
func cronCmd() { func newCronCmd() *cobra.Command {
if len(os.Args) < 3 { cmd := &cobra.Command{
cronHelp() Use: "cron",
return 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() cfg, err := loadConfig()
if err != nil { if err != nil {
fmt.Printf("Error loading config: %v\n", err) return "", fmt.Errorf("Error loading config: %w", 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 filepath.Join(cfg.WorkspacePath(), "cron", "jobs.json"), nil
} }
func cronHelp() { func runCronList(cmd *cobra.Command, args []string) error {
fmt.Println("\nCron commands:") storePath, err := getCronStorePath()
fmt.Println(" list List all scheduled jobs") if err != nil {
fmt.Println(" add Add a new scheduled job") fmt.Println(err)
fmt.Println(" remove <id> Remove a job by ID") return nil
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 cronListCmd(storePath string) {
cs := cron.NewCronService(storePath, nil) cs := cron.NewCronService(storePath, nil)
jobs := cs.ListJobs(true) // Show all jobs, including disabled jobs := cs.ListJobs(true)
if len(jobs) == 0 { if len(jobs) == 0 {
fmt.Println("No scheduled jobs.") fmt.Println("No scheduled jobs.")
return return nil
} }
fmt.Println("\nScheduled Jobs:") fmt.Println("\nScheduled Jobs:")
@ -105,75 +131,32 @@ func cronListCmd(storePath string) {
fmt.Printf(" Status: %s\n", status) fmt.Printf(" Status: %s\n", status)
fmt.Printf(" Next run: %s\n", nextRun) fmt.Printf(" Next run: %s\n", nextRun)
} }
return nil
} }
func cronAddCmd(storePath string) { func runCronAdd(cmd *cobra.Command, args []string) error {
name := "" storePath, err := getCronStorePath()
message := "" if err != nil {
var everySec *int64 fmt.Println(err)
cronExpr := "" return nil
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++
}
}
} }
if name == "" { name, _ := cmd.Flags().GetString("name")
fmt.Println("Error: --name is required") message, _ := cmd.Flags().GetString("message")
return 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 == "" { if everySec == 0 && cronExpr == "" {
fmt.Println("Error: --message is required")
return
}
if everySec == nil && cronExpr == "" {
fmt.Println("Error: Either --every or --cron must be specified") fmt.Println("Error: Either --every or --cron must be specified")
return return nil
} }
var schedule cron.CronSchedule var schedule cron.CronSchedule
if everySec != nil { if everySec != 0 {
everyMS := *everySec * 1000 everyMS := everySec * 1000
schedule = cron.CronSchedule{ schedule = cron.CronSchedule{
Kind: "every", Kind: "every",
EveryMS: &everyMS, EveryMS: &everyMS,
@ -189,39 +172,62 @@ func cronAddCmd(storePath string) {
job, err := cs.AddJob(name, schedule, message, deliver, channel, to) job, err := cs.AddJob(name, schedule, message, deliver, channel, to)
if err != nil { if err != nil {
fmt.Printf("Error adding job: %v\n", err) 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) cs := cron.NewCronService(storePath, nil)
if cs.RemoveJob(jobID) { if cs.RemoveJob(jobID) {
fmt.Printf("✓ Removed job %s\n", jobID) fmt.Printf("\u2713 Removed job %s\n", jobID)
} else { } 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) { func runCronEnable(cmd *cobra.Command, args []string) error {
if len(os.Args) < 4 { storePath, err := getCronStorePath()
fmt.Println("Usage: picoclaw cron enable/disable <job_id>") if err != nil {
return fmt.Println(err)
return nil
} }
jobID := os.Args[3] jobID := args[0]
cs := cron.NewCronService(storePath, nil) cs := cron.NewCronService(storePath, nil)
enabled := !disable job := cs.EnableJob(jobID, true)
job := cs.EnableJob(jobID, enabled)
if job != nil { if job != nil {
status := "enabled" fmt.Printf("\u2713 Job '%s' enabled\n", job.Name)
if disable {
status = "disabled"
}
fmt.Printf("✓ Job '%s' %s\n", job.Name, status)
} else { } 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
} }

View file

@ -25,17 +25,24 @@ import (
"github.com/sipeed/picoclaw/pkg/state" "github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/voice" "github.com/sipeed/picoclaw/pkg/voice"
"github.com/spf13/cobra"
) )
func gatewayCmd() { func newGatewayCmd() *cobra.Command {
// Check for --debug flag cmd := &cobra.Command{
args := os.Args[2:] Use: "gateway",
for _, arg := range args { Short: "Start picoclaw gateway",
if arg == "--debug" || arg == "-d" { RunE: runGateway,
logger.SetLevel(logger.DEBUG) }
fmt.Println("🔍 Debug mode enabled") cmd.Flags().BoolP("debug", "d", false, "Enable debug mode")
break 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() cfg, err := loadConfig()
@ -49,7 +56,6 @@ func gatewayCmd() {
fmt.Printf("Error creating provider: %v\n", err) fmt.Printf("Error creating provider: %v\n", err)
os.Exit(1) os.Exit(1)
} }
// Use the resolved model ID from provider creation
if modelID != "" { if modelID != "" {
cfg.Agents.Defaults.Model = modelID cfg.Agents.Defaults.Model = modelID
} }
@ -57,17 +63,15 @@ func gatewayCmd() {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
// Print agent startup info fmt.Println("\n\U0001f4e6 Agent Status:")
fmt.Println("\n📦 Agent Status:")
startupInfo := agentLoop.GetStartupInfo() startupInfo := agentLoop.GetStartupInfo()
toolsInfo := startupInfo["tools"].(map[string]interface{}) toolsInfo := startupInfo["tools"].(map[string]interface{})
skillsInfo := startupInfo["skills"].(map[string]interface{}) skillsInfo := startupInfo["skills"].(map[string]interface{})
fmt.Printf(" Tools: %d loaded\n", toolsInfo["count"]) fmt.Printf(" \u2022 Tools: %d loaded\n", toolsInfo["count"])
fmt.Printf(" Skills: %d/%d available\n", fmt.Printf(" \u2022 Skills: %d/%d available\n",
skillsInfo["available"], skillsInfo["available"],
skillsInfo["total"]) skillsInfo["total"])
// Log to file as well
logger.InfoCF("agent", "Agent initialized", logger.InfoCF("agent", "Agent initialized",
map[string]interface{}{ map[string]interface{}{
"tools_count": toolsInfo["count"], "tools_count": toolsInfo["count"],
@ -75,7 +79,6 @@ func gatewayCmd() {
"skills_available": skillsInfo["available"], "skills_available": skillsInfo["available"],
}) })
// Setup cron tool and service
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg) cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg)
@ -86,11 +89,9 @@ func gatewayCmd() {
) )
heartbeatService.SetBus(msgBus) heartbeatService.SetBus(msgBus)
heartbeatService.SetHandler(func(prompt, channel, chatID string) *tools.ToolResult { heartbeatService.SetHandler(func(prompt, channel, chatID string) *tools.ToolResult {
// Use cli:direct as fallback if no valid channel
if channel == "" || chatID == "" { if channel == "" || chatID == "" {
channel, chatID = "cli", "direct" channel, chatID = "cli", "direct"
} }
// Use ProcessHeartbeat - no session history, each heartbeat is independent
response, err := agentLoop.ProcessHeartbeat(context.Background(), prompt, channel, chatID) response, err := agentLoop.ProcessHeartbeat(context.Background(), prompt, channel, chatID)
if err != nil { if err != nil {
return tools.ErrorResult(fmt.Sprintf("Heartbeat error: %v", err)) return tools.ErrorResult(fmt.Sprintf("Heartbeat error: %v", err))
@ -98,8 +99,6 @@ func gatewayCmd() {
if response == "HEARTBEAT_OK" { if response == "HEARTBEAT_OK" {
return tools.SilentResult("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) return tools.SilentResult(response)
}) })
@ -109,7 +108,6 @@ func gatewayCmd() {
os.Exit(1) os.Exit(1)
} }
// Inject channel manager into agent loop for command handling
agentLoop.SetChannelManager(channelManager) agentLoop.SetChannelManager(channelManager)
var transcriber *voice.GroqTranscriber var transcriber *voice.GroqTranscriber
@ -141,12 +139,12 @@ func gatewayCmd() {
enabledChannels := channelManager.GetEnabledChannels() enabledChannels := channelManager.GetEnabledChannels()
if len(enabledChannels) > 0 { if len(enabledChannels) > 0 {
fmt.Printf(" Channels enabled: %s\n", enabledChannels) fmt.Printf("\u2713 Channels enabled: %s\n", enabledChannels)
} else { } 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") fmt.Println("Press Ctrl+C to stop")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -155,12 +153,12 @@ func gatewayCmd() {
if err := cronService.Start(); err != nil { if err := cronService.Start(); err != nil {
fmt.Printf("Error starting cron service: %v\n", err) 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 { if err := heartbeatService.Start(); err != nil {
fmt.Printf("Error starting heartbeat service: %v\n", err) 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()) stateManager := state.NewManager(cfg.WorkspacePath())
deviceService := devices.NewService(devices.Config{ deviceService := devices.NewService(devices.Config{
@ -171,7 +169,7 @@ func gatewayCmd() {
if err := deviceService.Start(ctx); err != nil { if err := deviceService.Start(ctx); err != nil {
fmt.Printf("Error starting device service: %v\n", err) fmt.Printf("Error starting device service: %v\n", err)
} else if cfg.Devices.Enabled { } 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 { 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()}) 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) go agentLoop.Run(ctx)
@ -200,20 +198,18 @@ func gatewayCmd() {
cronService.Stop() cronService.Stop()
agentLoop.Stop() agentLoop.Stop()
channelManager.StopAll(ctx) 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 { 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") cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
// Create cron service
cronService := cron.NewCronService(cronStorePath, nil) cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg) cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg)
agentLoop.RegisterTool(cronTool) agentLoop.RegisterTool(cronTool)
// Set the onJob handler
cronService.SetOnJob(func(job *cron.CronJob) (string, error) { cronService.SetOnJob(func(job *cron.CronJob) (string, error) {
result := cronTool.ExecuteJob(context.Background(), job) result := cronTool.ExecuteJob(context.Background(), job)
return result, nil return result, nil

View file

@ -8,45 +8,41 @@ import (
"os" "os"
"github.com/sipeed/picoclaw/pkg/migrate" "github.com/sipeed/picoclaw/pkg/migrate"
"github.com/spf13/cobra"
) )
func migrateCmd() { func newMigrateCmd() *cobra.Command {
if len(os.Args) > 2 && (os.Args[2] == "--help" || os.Args[2] == "-h") { cmd := &cobra.Command{
migrateHelp() Use: "migrate",
return 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{} opts := migrate.Options{}
opts.DryRun, _ = cmd.Flags().GetBool("dry-run")
args := os.Args[2:] opts.ConfigOnly, _ = cmd.Flags().GetBool("config-only")
for i := 0; i < len(args); i++ { opts.WorkspaceOnly, _ = cmd.Flags().GetBool("workspace-only")
switch args[i] { opts.Force, _ = cmd.Flags().GetBool("force")
case "--dry-run": opts.Refresh, _ = cmd.Flags().GetBool("refresh")
opts.DryRun = true opts.OpenClawHome, _ = cmd.Flags().GetString("openclaw-home")
case "--config-only": opts.PicoClawHome, _ = cmd.Flags().GetString("picoclaw-home")
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)
}
}
result, err := migrate.Run(opts) result, err := migrate.Run(opts)
if err != nil { if err != nil {
@ -57,25 +53,5 @@ func migrateCmd() {
if !opts.DryRun { if !opts.DryRun {
migrate.PrintSummary(result) migrate.PrintSummary(result)
} }
} return nil
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")
} }

View file

@ -11,13 +11,22 @@ import (
"path/filepath" "path/filepath"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/spf13/cobra"
) )
//go:generate cp -r ../../workspace . //go:generate cp -r ../../workspace .
//go:embed workspace //go:embed workspace
var embeddedFiles embed.FS 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() configPath := getConfigPath()
if _, err := os.Stat(configPath); err == nil { if _, err := os.Stat(configPath); err == nil {
@ -27,7 +36,7 @@ func onboard() {
fmt.Scanln(&response) fmt.Scanln(&response)
if response != "y" { if response != "y" {
fmt.Println("Aborted.") fmt.Println("Aborted.")
return return nil
} }
} }
@ -51,26 +60,23 @@ func onboard() {
fmt.Println(" See README.md for 17+ supported providers.") fmt.Println(" See README.md for 17+ supported providers.")
fmt.Println("") fmt.Println("")
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"") fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
return nil
} }
func copyEmbeddedToTarget(targetDir string) error { func copyEmbeddedToTarget(targetDir string) error {
// Ensure target directory exists
if err := os.MkdirAll(targetDir, 0755); err != nil { if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("Failed to create target directory: %w", err) 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 { err := fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err
} }
// Skip directories
if d.IsDir() { if d.IsDir() {
return nil return nil
} }
// Read embedded file
data, err := embeddedFiles.ReadFile(path) data, err := embeddedFiles.ReadFile(path)
if err != nil { if err != nil {
return fmt.Errorf("Failed to read embedded file %s: %w", path, err) 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) return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err)
} }
// Build target file path
targetPath := filepath.Join(targetDir, new_path) targetPath := filepath.Join(targetDir, new_path)
// Ensure target file's directory exists
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err) return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
} }
// Write file
if err := os.WriteFile(targetPath, data, 0644); err != nil { if err := os.WriteFile(targetPath, data, 0644); err != nil {
return fmt.Errorf("Failed to write file %s: %w", targetPath, err) return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
} }

View file

@ -14,33 +14,129 @@ import (
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/skills" "github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
"github.com/spf13/cobra"
) )
func skillsHelp() { type skillsContext struct {
fmt.Println("\nSkills commands:") installer *skills.SkillInstaller
fmt.Println(" list List installed skills") loader *skills.SkillsLoader
fmt.Println(" install <repo> Install skill from GitHub") workspace string
fmt.Println(" install-builtin Install all builtin skills to workspace") cfg *config.Config
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")
} }
func skillsListCmd(loader *skills.SkillsLoader) { func loadSkillsContext() (*skillsContext, error) {
allSkills := loader.ListSkills() 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 { if len(allSkills) == 0 {
fmt.Println("No skills installed.") fmt.Println("No skills installed.")
return return nil
} }
fmt.Println("\nInstalled Skills:") fmt.Println("\nInstalled Skills:")
@ -51,54 +147,55 @@ func skillsListCmd(loader *skills.SkillsLoader) {
fmt.Printf(" %s\n", skill.Description) fmt.Printf(" %s\n", skill.Description)
} }
} }
return nil
} }
func skillsInstallCmd(installer *skills.SkillInstaller, cfg *config.Config) { func runSkillsInstall(cmd *cobra.Command, args []string) error {
if len(os.Args) < 4 { sc, err := loadSkillsContext()
fmt.Println("Usage: picoclaw skills install <github-repo>") if err != nil {
fmt.Println(" picoclaw skills install --registry <name> <slug>") fmt.Println(err)
return return nil
} }
// Check for --registry flag. // Check for --registry flag
if os.Args[3] == "--registry" { registry, _ := cmd.Flags().GetString("registry")
if len(os.Args) < 6 { if registry != "" {
if len(args) < 2 {
fmt.Println("Usage: picoclaw skills install --registry <name> <slug>") fmt.Println("Usage: picoclaw skills install --registry <name> <slug>")
fmt.Println("Example: picoclaw skills install --registry clawhub github") fmt.Println("Example: picoclaw skills install --registry clawhub github")
return return nil
} }
registryName := os.Args[4] slug := args[1]
slug := os.Args[5] return skillsInstallFromRegistry(sc.cfg, registry, slug)
skillsInstallFromRegistry(cfg, registryName, slug)
return
} }
// Default: install from GitHub (backward compatible). // Default: install from GitHub
repo := os.Args[3] repo := args[0]
fmt.Printf("Installing skill from %s...\n", repo) fmt.Printf("Installing skill from %s...\n", repo)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
if err := installer.InstallFromGitHub(ctx, repo); err != nil { if err := sc.installer.InstallFromGitHub(ctx, repo); err != nil {
fmt.Printf("\u2717 Failed to install skill: %v\n", err) fmt.Printf(" Failed to install skill: %v\n", err)
os.Exit(1) 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). // 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) err := utils.ValidateSkillIdentifier(registryName)
if err != nil { if err != nil {
fmt.Printf("\u2717 Invalid registry name: %v\n", err) fmt.Printf(" Invalid registry name: %v\n", err)
os.Exit(1) os.Exit(1)
} }
err = utils.ValidateSkillIdentifier(slug) err = utils.ValidateSkillIdentifier(slug)
if err != nil { if err != nil {
fmt.Printf("\u2717 Invalid slug: %v\n", err) fmt.Printf(" Invalid slug: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@ -111,7 +208,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
registry := registryMgr.GetRegistry(registryName) registry := registryMgr.GetRegistry(registryName)
if registry == nil { 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) os.Exit(1)
} }
@ -119,7 +216,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
targetDir := filepath.Join(workspace, "skills", slug) targetDir := filepath.Join(workspace, "skills", slug)
if _, err := os.Stat(targetDir); err == nil { 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) os.Exit(1)
} }
@ -127,7 +224,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
defer cancel() defer cancel()
if err := os.MkdirAll(filepath.Join(workspace, "skills"), 0755); err != nil { 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) os.Exit(1)
} }
@ -135,45 +232,60 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) {
if err != nil { if err != nil {
rmErr := os.RemoveAll(targetDir) rmErr := os.RemoveAll(targetDir)
if rmErr != nil { 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) os.Exit(1)
} }
if result.IsMalwareBlocked { if result.IsMalwareBlocked {
rmErr := os.RemoveAll(targetDir) rmErr := os.RemoveAll(targetDir)
if rmErr != nil { 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) os.Exit(1)
} }
if result.IsSuspicious { 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 != "" { if result.Summary != "" {
fmt.Printf(" %s\n", 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) 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) fmt.Printf("✗ Failed to remove skill: %v\n", err)
os.Exit(1) os.Exit(1)
} }
fmt.Printf("✓ Skill '%s' removed successfully!\n", skillName) 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" builtinSkillsDir := "./picoclaw/skills"
workspaceSkillsDir := filepath.Join(workspace, "skills") workspaceSkillsDir := filepath.Join(sc.workspace, "skills")
fmt.Printf("Copying builtin skills to workspace...\n") 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("\n✓ All builtin skills installed!")
fmt.Println("Now you can use them in your workspace.") 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() cfg, err := loadConfig()
if err != nil { if err != nil {
fmt.Printf("Error loading config: %v\n", err) fmt.Printf("Error loading config: %v\n", err)
return return nil
} }
builtinSkillsDir := filepath.Join(filepath.Dir(cfg.WorkspacePath()), "picoclaw", "skills") builtinSkillsDir := filepath.Join(filepath.Dir(cfg.WorkspacePath()), "picoclaw", "skills")
@ -221,12 +334,12 @@ func skillsListBuiltinCmd() {
entries, err := os.ReadDir(builtinSkillsDir) entries, err := os.ReadDir(builtinSkillsDir)
if err != nil { if err != nil {
fmt.Printf("Error reading builtin skills: %v\n", err) fmt.Printf("Error reading builtin skills: %v\n", err)
return return nil
} }
if len(entries) == 0 { if len(entries) == 0 {
fmt.Println("No builtin skills available.") fmt.Println("No builtin skills available.")
return return nil
} }
for _, entry := range entries { 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...") fmt.Println("Searching for available skills...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
availableSkills, err := installer.ListAvailableSkills(ctx) availableSkills, err := sc.installer.ListAvailableSkills(ctx)
if err != nil { if err != nil {
fmt.Printf("✗ Failed to fetch skills list: %v\n", err) fmt.Printf("✗ Failed to fetch skills list: %v\n", err)
return return nil
} }
if len(availableSkills) == 0 { if len(availableSkills) == 0 {
fmt.Println("No skills available.") fmt.Println("No skills available.")
return return nil
} }
fmt.Printf("\nAvailable Skills (%d):\n", len(availableSkills)) fmt.Printf("\nAvailable Skills (%d):\n", len(availableSkills))
@ -290,16 +410,25 @@ func skillsSearchCmd(installer *skills.SkillInstaller) {
} }
fmt.Println() fmt.Println()
} }
return nil
} }
func skillsShowCmd(loader *skills.SkillsLoader, skillName string) { func runSkillsShow(cmd *cobra.Command, args []string) error {
content, ok := loader.LoadSkill(skillName) sc, err := loadSkillsContext()
if err != nil {
fmt.Println(err)
return nil
}
skillName := args[0]
content, ok := sc.loader.LoadSkill(skillName)
if !ok { if !ok {
fmt.Printf("✗ Skill '%s' not found\n", skillName) fmt.Printf("✗ Skill '%s' not found\n", skillName)
return return nil
} }
fmt.Printf("\n📦 Skill: %s\n", skillName) fmt.Printf("\n📦 Skill: %s\n", skillName)
fmt.Println("----------------------") fmt.Println("----------------------")
fmt.Println(content) fmt.Println(content)
return nil
} }

View file

@ -8,13 +8,22 @@ import (
"os" "os"
"github.com/sipeed/picoclaw/pkg/auth" "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() cfg, err := loadConfig()
if err != nil { if err != nil {
fmt.Printf("Error loading config: %v\n", err) fmt.Printf("Error loading config: %v\n", err)
return return nil
} }
configPath := getConfigPath() configPath := getConfigPath()
@ -28,16 +37,16 @@ func statusCmd() {
fmt.Println() fmt.Println()
if _, err := os.Stat(configPath); err == nil { if _, err := os.Stat(configPath); err == nil {
fmt.Println("Config:", configPath, "") fmt.Println("Config:", configPath, "\u2713")
} else { } else {
fmt.Println("Config:", configPath, "") fmt.Println("Config:", configPath, "\u2717")
} }
workspace := cfg.WorkspacePath() workspace := cfg.WorkspacePath()
if _, err := os.Stat(workspace); err == nil { if _, err := os.Stat(workspace); err == nil {
fmt.Println("Workspace:", workspace, "") fmt.Println("Workspace:", workspace, "\u2713")
} else { } else {
fmt.Println("Workspace:", workspace, "") fmt.Println("Workspace:", workspace, "\u2717")
} }
if _, err := os.Stat(configPath); err == nil { if _, err := os.Stat(configPath); err == nil {
@ -59,7 +68,7 @@ func statusCmd() {
status := func(enabled bool) string { status := func(enabled bool) string {
if enabled { if enabled {
return "" return "\u2713"
} }
return "not set" return "not set"
} }
@ -75,12 +84,12 @@ func statusCmd() {
fmt.Println("VolcEngine API:", status(hasVolcEngine)) fmt.Println("VolcEngine API:", status(hasVolcEngine))
fmt.Println("Nvidia API:", status(hasNvidia)) fmt.Println("Nvidia API:", status(hasNvidia))
if hasVLLM { if hasVLLM {
fmt.Printf("vLLM/Local: %s\n", cfg.Providers.VLLM.APIBase) fmt.Printf("vLLM/Local: \u2713 %s\n", cfg.Providers.VLLM.APIBase)
} else { } else {
fmt.Println("vLLM/Local: not set") fmt.Println("vLLM/Local: not set")
} }
if hasOllama { if hasOllama {
fmt.Printf("Ollama: %s\n", cfg.Providers.Ollama.APIBase) fmt.Printf("Ollama: \u2713 %s\n", cfg.Providers.Ollama.APIBase)
} else { } else {
fmt.Println("Ollama: not set") fmt.Println("Ollama: not set")
} }
@ -99,4 +108,5 @@ func statusCmd() {
} }
} }
} }
return nil
} }

View file

@ -14,14 +14,14 @@ import (
"runtime" "runtime"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/skills" "github.com/spf13/cobra"
) )
var ( var (
version = "dev" version = "dev"
gitCommit string gitCommit string
buildTime string buildTime string
goVersion string goVersion string
) )
const logo = "🦞" const logo = "🦞"
@ -92,101 +92,40 @@ func copyDirectory(src, dst string) error {
}) })
} }
func main() { var rootCmd = &cobra.Command{
if len(os.Args) < 2 { Use: "picoclaw",
printHelp() Short: fmt.Sprintf("%s picoclaw - Personal AI Assistant", logo),
os.Exit(1) }
}
command := os.Args[1] func newVersionCmd() *cobra.Command {
return &cobra.Command{
switch command { Use: "version",
case "onboard": Short: "Show version information",
onboard() Run: func(cmd *cobra.Command, args []string) {
case "agent": printVersion()
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 printHelp() { func init() {
fmt.Printf("%s picoclaw - Personal AI Assistant v%s\n\n", logo, version) rootCmd.AddCommand(
fmt.Println("Usage: picoclaw <command>") newOnboardCmd(),
fmt.Println() newAgentCmd(),
fmt.Println("Commands:") newGatewayCmd(),
fmt.Println(" onboard Initialize picoclaw configuration and workspace") newStatusCmd(),
fmt.Println(" agent Interact with the agent directly") newMigrateCmd(),
fmt.Println(" auth Manage authentication (login, logout, status)") newAuthCmd(),
fmt.Println(" gateway Start picoclaw gateway") newCronCmd(),
fmt.Println(" status Show picoclaw status") newSkillsCmd(),
fmt.Println(" cron Manage scheduled tasks") newVersionCmd(),
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw") )
fmt.Println(" skills Manage skills (install, list, remove)") rootCmd.Version = formatVersion()
fmt.Println(" version Show version information") }
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
} }
func getConfigPath() string { func getConfigPath() string {

3
go.mod
View file

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

9
go.sum
View file

@ -25,6 +25,7 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/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 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= 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/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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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= 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.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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= 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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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= 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 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 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 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 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= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=