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/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) {
|
||||||
|
|
|
||||||
|
|
@ -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" ||
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
3
go.mod
|
|
@ -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
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/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=
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue