This commit is contained in:
lpf
2026-02-20 18:48:57 +08:00
parent 34d883fd6b
commit d229e5de59
12 changed files with 1924 additions and 1895 deletions

157
cmd/clawgo/cli_common.go Normal file
View File

@@ -0,0 +1,157 @@
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"clawgo/pkg/config"
"clawgo/pkg/logger"
)
func copyDirectory(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
})
}
func normalizeCLIArgs(args []string) []string {
if len(args) == 0 {
return args
}
normalized := []string{args[0]}
for i := 1; i < len(args); i++ {
arg := args[i]
if arg == "--debug" || arg == "-d" {
continue
}
if arg == "--config" {
if i+1 < len(args) {
i++
}
continue
}
if strings.HasPrefix(arg, "--config=") {
continue
}
normalized = append(normalized, arg)
}
return normalized
}
func detectConfigPathFromArgs(args []string) string {
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--config" && i+1 < len(args) {
return strings.TrimSpace(args[i+1])
}
if strings.HasPrefix(arg, "--config=") {
return strings.TrimSpace(strings.TrimPrefix(arg, "--config="))
}
}
return ""
}
func printHelp() {
fmt.Printf("%s clawgo - Personal AI Assistant v%s\n\n", logo, version)
fmt.Println("Usage: clawgo <command> [options]")
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" onboard Initialize clawgo configuration and workspace")
fmt.Println(" agent Interact with the agent directly")
fmt.Println(" gateway Register/manage gateway service")
fmt.Println(" status Show clawgo status")
fmt.Println(" config Get/set config values")
fmt.Println(" cron Manage scheduled tasks")
fmt.Println(" login Configure CLIProxyAPI upstream")
fmt.Println(" channel Test and manage messaging channels")
fmt.Println(" skills Manage skills (install, list, remove)")
fmt.Println(" uninstall Uninstall clawgo components")
fmt.Println(" version Show version information")
fmt.Println()
fmt.Println("Global options:")
fmt.Println(" --config <path> Use custom config file")
fmt.Println(" --debug, -d Enable debug logging")
fmt.Println()
fmt.Println("Gateway service:")
fmt.Println(" clawgo gateway # register service")
fmt.Println(" clawgo gateway start|stop|restart|status")
fmt.Println(" clawgo gateway run # run foreground")
fmt.Println()
fmt.Println("Uninstall:")
fmt.Println(" clawgo uninstall # remove gateway service")
fmt.Println(" clawgo uninstall --purge # also remove config/workspace dir")
fmt.Println(" clawgo uninstall --remove-bin # also remove current executable")
}
func getConfigPath() string {
if strings.TrimSpace(globalConfigPathOverride) != "" {
return globalConfigPathOverride
}
if fromEnv := strings.TrimSpace(os.Getenv("CLAWGO_CONFIG")); fromEnv != "" {
return fromEnv
}
args := os.Args
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--config" && i+1 < len(args) {
return args[i+1]
}
if strings.HasPrefix(arg, "--config=") {
return strings.TrimPrefix(arg, "--config=")
}
}
return filepath.Join(config.GetConfigDir(), "config.json")
}
func loadConfig() (*config.Config, error) {
cfg, err := config.LoadConfig(getConfigPath())
if err != nil {
return nil, err
}
configureLogging(cfg)
return cfg, nil
}
func configureLogging(cfg *config.Config) {
if !cfg.Logging.Enabled {
logger.DisableFileLogging()
return
}
logFile := cfg.LogFilePath()
if err := logger.EnableFileLoggingWithRotation(logFile, cfg.Logging.MaxSizeMB, cfg.Logging.RetentionDays); err != nil {
fmt.Printf("Warning: failed to enable file logging: %v\n", err)
}
}

170
cmd/clawgo/cmd_agent.go Normal file
View File

@@ -0,0 +1,170 @@
package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"clawgo/pkg/agent"
"clawgo/pkg/bus"
"clawgo/pkg/cron"
"clawgo/pkg/logger"
"clawgo/pkg/providers"
"github.com/chzyer/readline"
)
func agentCmd() {
message := ""
sessionKey := "cli:default"
args := os.Args[2:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "--debug", "-d":
logger.SetLevel(logger.DEBUG)
fmt.Println("🔍 Debug mode enabled")
case "-m", "--message":
if i+1 < len(args) {
message = args[i+1]
i++
}
case "-s", "--session":
if i+1 < len(args) {
sessionKey = args[i+1]
i++
}
}
}
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
provider, err := providers.CreateProvider(cfg)
if err != nil {
fmt.Printf("Error creating provider: %v\n", err)
os.Exit(1)
}
msgBus := bus.NewMessageBus()
cronStorePath := filepath.Join(filepath.Dir(getConfigPath()), "cron", "jobs.json")
cronService := cron.NewCronService(cronStorePath, nil)
configureCronServiceRuntime(cronService, cfg)
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider, cronService)
startupInfo := agentLoop.GetStartupInfo()
logger.InfoCF("agent", "Agent initialized",
map[string]interface{}{
"tools_count": startupInfo["tools"].(map[string]interface{})["count"],
"skills_total": startupInfo["skills"].(map[string]interface{})["total"],
"skills_available": startupInfo["skills"].(map[string]interface{})["available"],
})
if message != "" {
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("\n%s %s\n", logo, response)
} else {
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
interactiveMode(agentLoop, sessionKey)
}
}
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
prompt := fmt.Sprintf("%s You: ", logo)
rl, err := readline.NewEx(&readline.Config{
Prompt: prompt,
HistoryFile: filepath.Join(os.TempDir(), ".clawgo_history"),
HistoryLimit: 100,
InterruptPrompt: "^C",
EOFPrompt: "exit",
})
if err != nil {
fmt.Printf("Error initializing readline: %v\n", err)
fmt.Println("Falling back to simple input mode...")
simpleInteractiveMode(agentLoop, sessionKey)
return
}
defer rl.Close()
for {
line, err := rl.Readline()
if err != nil {
if err == readline.ErrInterrupt || err == io.EOF {
fmt.Println("\nGoodbye!")
return
}
fmt.Printf("Error reading input: %v\n", err)
continue
}
input := strings.TrimSpace(line)
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("Goodbye!")
return
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", logo, response)
}
}
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print(fmt.Sprintf("%s You: ", logo))
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Println("\nGoodbye!")
return
}
fmt.Printf("Error reading input: %v\n", err)
continue
}
input := strings.TrimSpace(line)
if input == "" {
continue
}
if input == "exit" || input == "quit" {
fmt.Println("Goodbye!")
return
}
ctx := context.Background()
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("\n%s %s\n\n", logo, response)
}
}

96
cmd/clawgo/cmd_channel.go Normal file
View File

@@ -0,0 +1,96 @@
package main
import (
"context"
"fmt"
"os"
"clawgo/pkg/bus"
"clawgo/pkg/channels"
)
func channelCmd() {
if len(os.Args) < 3 {
channelHelp()
return
}
subcommand := os.Args[2]
switch subcommand {
case "test":
channelTestCmd()
default:
fmt.Printf("Unknown channel command: %s\n", subcommand)
channelHelp()
}
}
func channelHelp() {
fmt.Println("\nChannel commands:")
fmt.Println(" test Send a test message to a specific channel")
fmt.Println()
fmt.Println("Test options:")
fmt.Println(" --to Recipient ID")
fmt.Println(" --channel Channel name (telegram, discord, etc.)")
fmt.Println(" -m, --message Message to send")
}
func channelTestCmd() {
to := ""
channelName := ""
message := "This is a test message from ClawGo 🦞"
args := os.Args[3:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "--to":
if i+1 < len(args) {
to = args[i+1]
i++
}
case "--channel":
if i+1 < len(args) {
channelName = args[i+1]
i++
}
case "-m", "--message":
if i+1 < len(args) {
message = args[i+1]
i++
}
}
}
if channelName == "" || to == "" {
fmt.Println("Error: --channel and --to are required")
return
}
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
msgBus := bus.NewMessageBus()
mgr, err := channels.NewManager(cfg, msgBus)
if err != nil {
fmt.Printf("Error creating channel manager: %v\n", err)
os.Exit(1)
}
ctx := context.Background()
if err := mgr.StartAll(ctx); err != nil {
fmt.Printf("Error starting channels: %v\n", err)
os.Exit(1)
}
fmt.Printf("Sending test message to %s (%s)...\n", channelName, to)
if err := mgr.SendToChannel(ctx, channelName, to, message); err != nil {
fmt.Printf("✗ Failed to send message: %v\n", err)
os.Exit(1)
}
fmt.Println("✓ Test message sent successfully!")
}

201
cmd/clawgo/cmd_config.go Normal file
View File

@@ -0,0 +1,201 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"clawgo/pkg/config"
"clawgo/pkg/configops"
)
func configCmd() {
if len(os.Args) < 3 {
configHelp()
return
}
switch os.Args[2] {
case "set":
configSetCmd()
case "get":
configGetCmd()
case "check":
configCheckCmd()
case "reload":
configReloadCmd()
default:
fmt.Printf("Unknown config command: %s\n", os.Args[2])
configHelp()
}
}
func configHelp() {
fmt.Println("\nConfig commands:")
fmt.Println(" set <path> <value> Set config value and trigger hot reload")
fmt.Println(" get <path> Get config value")
fmt.Println(" check Validate current config")
fmt.Println(" reload Trigger gateway hot reload")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" clawgo config set channels.telegram.enabled true")
fmt.Println(" clawgo config set channels.telegram.enable true")
fmt.Println(" clawgo config get providers.proxy.api_base")
fmt.Println(" clawgo config check")
fmt.Println(" clawgo config reload")
}
func configSetCmd() {
if len(os.Args) < 5 {
fmt.Println("Usage: clawgo config set <path> <value>")
return
}
configPath := getConfigPath()
cfgMap, err := loadConfigAsMap(configPath)
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
path := normalizeConfigPath(os.Args[3])
args := os.Args[4:]
valueParts := make([]string, 0, len(args))
for i := 0; i < len(args); i++ {
part := args[i]
if part == "--debug" || part == "-d" {
continue
}
if part == "--config" {
i++
continue
}
if strings.HasPrefix(part, "--config=") {
continue
}
valueParts = append(valueParts, part)
}
if len(valueParts) == 0 {
fmt.Println("Error: value is required")
return
}
value := parseConfigValue(strings.Join(valueParts, " "))
if err := setMapValueByPath(cfgMap, path, value); err != nil {
fmt.Printf("Error setting value: %v\n", err)
return
}
data, err := json.MarshalIndent(cfgMap, "", " ")
if err != nil {
fmt.Printf("Error serializing config: %v\n", err)
return
}
backupPath, err := writeConfigAtomicWithBackup(configPath, data)
if err != nil {
fmt.Printf("Error writing config: %v\n", err)
return
}
fmt.Printf("✓ Updated %s = %v\n", path, value)
running, err := triggerGatewayReload()
if err != nil {
if running {
if rbErr := rollbackConfigFromBackup(configPath, backupPath); rbErr != nil {
fmt.Printf("Hot reload failed and rollback failed: %v\n", rbErr)
} else {
fmt.Printf("Hot reload failed, config rolled back: %v\n", err)
}
return
}
fmt.Printf("Updated config file. Hot reload not applied: %v\n", err)
} else {
fmt.Println("✓ Gateway hot reload signal sent")
}
}
func configGetCmd() {
if len(os.Args) < 4 {
fmt.Println("Usage: clawgo config get <path>")
return
}
configPath := getConfigPath()
cfgMap, err := loadConfigAsMap(configPath)
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
path := normalizeConfigPath(os.Args[3])
value, ok := getMapValueByPath(cfgMap, path)
if !ok {
fmt.Printf("Path not found: %s\n", path)
return
}
data, err := json.Marshal(value)
if err != nil {
fmt.Printf("%v\n", value)
return
}
fmt.Println(string(data))
}
func configReloadCmd() {
if _, err := triggerGatewayReload(); err != nil {
fmt.Printf("Hot reload not applied: %v\n", err)
return
}
fmt.Println("✓ Gateway hot reload signal sent")
}
func configCheckCmd() {
cfg, err := config.LoadConfig(getConfigPath())
if err != nil {
fmt.Printf("Config load failed: %v\n", err)
return
}
validationErrors := config.Validate(cfg)
if len(validationErrors) == 0 {
fmt.Println("✓ Config validation passed")
return
}
fmt.Println("✗ Config validation failed:")
for _, ve := range validationErrors {
fmt.Printf(" - %v\n", ve)
}
}
func loadConfigAsMap(path string) (map[string]interface{}, error) {
return configops.LoadConfigAsMap(path)
}
func normalizeConfigPath(path string) string {
return configops.NormalizeConfigPath(path)
}
func parseConfigValue(raw string) interface{} {
return configops.ParseConfigValue(raw)
}
func setMapValueByPath(root map[string]interface{}, path string, value interface{}) error {
return configops.SetMapValueByPath(root, path, value)
}
func getMapValueByPath(root map[string]interface{}, path string) (interface{}, bool) {
return configops.GetMapValueByPath(root, path)
}
func writeConfigAtomicWithBackup(configPath string, data []byte) (string, error) {
return configops.WriteConfigAtomicWithBackup(configPath, data)
}
func rollbackConfigFromBackup(configPath, backupPath string) error {
return configops.RollbackConfigFromBackup(configPath, backupPath)
}
func triggerGatewayReload() (bool, error) {
return configops.TriggerGatewayReload(getConfigPath(), errGatewayNotRunning)
}

212
cmd/clawgo/cmd_cron.go Normal file
View File

@@ -0,0 +1,212 @@
package main
import (
"fmt"
"os"
"path/filepath"
"time"
"clawgo/pkg/cron"
)
func cronCmd() {
if len(os.Args) < 3 {
cronHelp()
return
}
subcommand := os.Args[2]
dataDir := filepath.Join(filepath.Dir(getConfigPath()), "cron")
cronStorePath := filepath.Join(dataDir, "jobs.json")
switch subcommand {
case "list":
cronListCmd(cronStorePath)
case "add":
cronAddCmd(cronStorePath)
case "remove":
if len(os.Args) < 4 {
fmt.Println("Usage: clawgo 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()
}
}
func cronHelp() {
fmt.Println("\nCron commands:")
fmt.Println(" list List all scheduled jobs")
fmt.Println(" add Add a new scheduled job")
fmt.Println(" remove <id> Remove a job by ID")
fmt.Println(" enable <id> Enable a job")
fmt.Println(" disable <id> Disable a job")
fmt.Println()
fmt.Println("Add options:")
fmt.Println(" -n, --name Job name")
fmt.Println(" -m, --message Message for agent")
fmt.Println(" -e, --every Run every N seconds")
fmt.Println(" -c, --cron Cron expression (e.g. '0 9 * * *')")
fmt.Println(" -d, --deliver Deliver response to channel")
fmt.Println(" --to Recipient for delivery")
fmt.Println(" --channel Channel for delivery")
}
func cronListCmd(storePath string) {
cs := cron.NewCronService(storePath, nil)
jobs := cs.ListJobs(false)
if len(jobs) == 0 {
fmt.Println("No scheduled jobs.")
return
}
fmt.Println("\nScheduled Jobs:")
fmt.Println("----------------")
for _, job := range jobs {
var schedule string
if job.Schedule.Kind == "every" && job.Schedule.EveryMS != nil {
schedule = fmt.Sprintf("every %ds", *job.Schedule.EveryMS/1000)
} else if job.Schedule.Kind == "cron" {
schedule = job.Schedule.Expr
} else {
schedule = "one-time"
}
nextRun := "scheduled"
if job.State.NextRunAtMS != nil {
nextTime := time.UnixMilli(*job.State.NextRunAtMS)
nextRun = nextTime.Format("2006-01-02 15:04")
}
status := "enabled"
if !job.Enabled {
status = "disabled"
}
fmt.Printf(" %s (%s)\n", job.Name, job.ID)
fmt.Printf(" Schedule: %s\n", schedule)
fmt.Printf(" Status: %s\n", status)
fmt.Printf(" Next run: %s\n", nextRun)
}
}
func cronAddCmd(storePath string) {
name := ""
message := ""
var everySec *int64
cronExpr := ""
deliver := false
channel := ""
to := ""
args := os.Args[3:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "-n", "--name":
if i+1 < len(args) {
name = args[i+1]
i++
}
case "-m", "--message":
if i+1 < len(args) {
message = args[i+1]
i++
}
case "-e", "--every":
if i+1 < len(args) {
var sec int64
fmt.Sscanf(args[i+1], "%d", &sec)
everySec = &sec
i++
}
case "-c", "--cron":
if i+1 < len(args) {
cronExpr = args[i+1]
i++
}
case "-d", "--deliver":
deliver = true
case "--to":
if i+1 < len(args) {
to = args[i+1]
i++
}
case "--channel":
if i+1 < len(args) {
channel = args[i+1]
i++
}
}
}
if name == "" {
fmt.Println("Error: --name is required")
return
}
if message == "" {
fmt.Println("Error: --message is required")
return
}
if everySec == nil && cronExpr == "" {
fmt.Println("Error: Either --every or --cron must be specified")
return
}
var schedule cron.CronSchedule
if everySec != nil {
everyMS := *everySec * 1000
schedule = cron.CronSchedule{Kind: "every", EveryMS: &everyMS}
} else {
schedule = cron.CronSchedule{Kind: "cron", Expr: cronExpr}
}
cs := cron.NewCronService(storePath, nil)
job, err := cs.AddJob(name, schedule, message, deliver, channel, to)
if err != nil {
fmt.Printf("Error adding job: %v\n", err)
return
}
fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID)
}
func cronRemoveCmd(storePath, jobID string) {
cs := cron.NewCronService(storePath, nil)
if cs.RemoveJob(jobID) {
fmt.Printf("✓ Removed job %s\n", jobID)
} else {
fmt.Printf("✗ Job %s not found\n", jobID)
}
}
func cronEnableCmd(storePath string, disable bool) {
if len(os.Args) < 4 {
fmt.Println("Usage: clawgo cron enable/disable <job_id>")
return
}
jobID := os.Args[3]
cs := cron.NewCronService(storePath, nil)
enabled := !disable
job := cs.EnableJob(jobID, enabled)
if job != nil {
status := "enabled"
if disable {
status = "disabled"
}
fmt.Printf("✓ Job '%s' %s\n", job.Name, status)
} else {
fmt.Printf("✗ Job %s not found\n", jobID)
}
}

523
cmd/clawgo/cmd_gateway.go Normal file
View File

@@ -0,0 +1,523 @@
package main
import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"reflect"
"runtime"
"strings"
"syscall"
"time"
"clawgo/pkg/agent"
"clawgo/pkg/bus"
"clawgo/pkg/channels"
"clawgo/pkg/config"
"clawgo/pkg/cron"
"clawgo/pkg/heartbeat"
"clawgo/pkg/logger"
"clawgo/pkg/providers"
"clawgo/pkg/sentinel"
"clawgo/pkg/voice"
)
func gatewayCmd() {
args := os.Args[2:]
if len(args) == 0 {
if err := gatewayInstallServiceCmd(); err != nil {
fmt.Printf("Error registering gateway service: %v\n", err)
os.Exit(1)
}
return
}
switch args[0] {
case "run":
case "start", "stop", "restart", "status":
if err := gatewayServiceControlCmd(args[0]); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
return
default:
fmt.Printf("Unknown gateway command: %s\n", args[0])
fmt.Println("Usage: clawgo gateway [run|start|stop|restart|status]")
return
}
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") {
applyMaximumPermissionPolicy(cfg)
}
msgBus := bus.NewMessageBus()
cronStorePath := filepath.Join(filepath.Dir(getConfigPath()), "cron", "jobs.json")
cronService := cron.NewCronService(cronStorePath, nil)
configureCronServiceRuntime(cronService, cfg)
heartbeatService := heartbeat.NewHeartbeatService(cfg.WorkspacePath(), nil, 30*60, true)
sentinelService := sentinel.NewService(
getConfigPath(),
cfg.WorkspacePath(),
cfg.Sentinel.IntervalSec,
cfg.Sentinel.AutoHeal,
func(message string) {
if cfg.Sentinel.NotifyChannel != "" && cfg.Sentinel.NotifyChatID != "" {
msgBus.PublishOutbound(bus.OutboundMessage{
Channel: cfg.Sentinel.NotifyChannel,
ChatID: cfg.Sentinel.NotifyChatID,
Content: "[Sentinel] " + message,
})
}
},
)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
agentLoop, channelManager, err := buildGatewayRuntime(ctx, cfg, msgBus, cronService)
if err != nil {
fmt.Printf("Error initializing gateway runtime: %v\n", err)
os.Exit(1)
}
sentinelService.SetManager(channelManager)
pidFile := filepath.Join(filepath.Dir(getConfigPath()), "gateway.pid")
if err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644); err != nil {
fmt.Printf("Warning: failed to write PID file: %v\n", err)
} else {
defer os.Remove(pidFile)
}
enabledChannels := channelManager.GetEnabledChannels()
if len(enabledChannels) > 0 {
fmt.Printf("✓ Channels enabled: %s\n", enabledChannels)
} else {
fmt.Println("⚠ Warning: No channels enabled")
}
fmt.Printf("✓ Gateway started on %s:%d\n", cfg.Gateway.Host, cfg.Gateway.Port)
fmt.Println("Press Ctrl+C to stop. Send SIGHUP to hot-reload config.")
if err := cronService.Start(); err != nil {
fmt.Printf("Error starting cron service: %v\n", err)
}
fmt.Println("✓ Cron service started")
if err := heartbeatService.Start(); err != nil {
fmt.Printf("Error starting heartbeat service: %v\n", err)
}
fmt.Println("✓ Heartbeat service started")
if cfg.Sentinel.Enabled {
sentinelService.Start()
fmt.Println("✓ Sentinel service started")
}
if err := channelManager.StartAll(ctx); err != nil {
fmt.Printf("Error starting channels: %v\n", err)
}
go agentLoop.Run(ctx)
go runGatewayStartupCompactionCheck(ctx, agentLoop)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
for {
sig := <-sigChan
switch sig {
case syscall.SIGHUP:
fmt.Println("\n↻ Reloading config...")
newCfg, err := config.LoadConfig(getConfigPath())
if err != nil {
fmt.Printf("✗ Reload failed (load config): %v\n", err)
continue
}
if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") {
applyMaximumPermissionPolicy(newCfg)
}
configureCronServiceRuntime(cronService, newCfg)
if reflect.DeepEqual(cfg, newCfg) {
fmt.Println("✓ Config unchanged, skip reload")
continue
}
runtimeSame := reflect.DeepEqual(cfg.Agents, newCfg.Agents) &&
reflect.DeepEqual(cfg.Providers, newCfg.Providers) &&
reflect.DeepEqual(cfg.Tools, newCfg.Tools) &&
reflect.DeepEqual(cfg.Channels, newCfg.Channels)
if runtimeSame {
configureLogging(newCfg)
sentinelService.Stop()
sentinelService = sentinel.NewService(
getConfigPath(),
newCfg.WorkspacePath(),
newCfg.Sentinel.IntervalSec,
newCfg.Sentinel.AutoHeal,
func(message string) {
if newCfg.Sentinel.NotifyChannel != "" && newCfg.Sentinel.NotifyChatID != "" {
msgBus.PublishOutbound(bus.OutboundMessage{
Channel: newCfg.Sentinel.NotifyChannel,
ChatID: newCfg.Sentinel.NotifyChatID,
Content: "[Sentinel] " + message,
})
}
},
)
if newCfg.Sentinel.Enabled {
sentinelService.SetManager(channelManager)
sentinelService.Start()
}
cfg = newCfg
fmt.Println("✓ Config hot-reload applied (logging/metadata only)")
continue
}
newAgentLoop, newChannelManager, err := buildGatewayRuntime(ctx, newCfg, msgBus, cronService)
if err != nil {
fmt.Printf("✗ Reload failed (init runtime): %v\n", err)
continue
}
channelManager.StopAll(ctx)
agentLoop.Stop()
channelManager = newChannelManager
agentLoop = newAgentLoop
cfg = newCfg
sentinelService.Stop()
sentinelService = sentinel.NewService(
getConfigPath(),
newCfg.WorkspacePath(),
newCfg.Sentinel.IntervalSec,
newCfg.Sentinel.AutoHeal,
func(message string) {
if newCfg.Sentinel.NotifyChannel != "" && newCfg.Sentinel.NotifyChatID != "" {
msgBus.PublishOutbound(bus.OutboundMessage{
Channel: newCfg.Sentinel.NotifyChannel,
ChatID: newCfg.Sentinel.NotifyChatID,
Content: "[Sentinel] " + message,
})
}
},
)
if newCfg.Sentinel.Enabled {
sentinelService.Start()
}
sentinelService.SetManager(channelManager)
if err := channelManager.StartAll(ctx); err != nil {
fmt.Printf("✗ Reload failed (start channels): %v\n", err)
continue
}
go agentLoop.Run(ctx)
fmt.Println("✓ Config hot-reload applied")
default:
fmt.Println("\nShutting down...")
cancel()
heartbeatService.Stop()
sentinelService.Stop()
cronService.Stop()
agentLoop.Stop()
channelManager.StopAll(ctx)
fmt.Println("✓ Gateway stopped")
return
}
}
}
func runGatewayStartupCompactionCheck(parent context.Context, agentLoop *agent.AgentLoop) {
if agentLoop == nil {
return
}
checkCtx, cancel := context.WithTimeout(parent, 10*time.Minute)
defer cancel()
report := agentLoop.RunStartupSelfCheckAllSessions(checkCtx)
logger.InfoCF("gateway", "Startup compaction check completed", map[string]interface{}{
"sessions_total": report.TotalSessions,
"sessions_compacted": report.CompactedSessions,
})
}
func maybePromptAndEscalateRoot(command string) {
if os.Getenv(envRootPrompted) == "1" {
return
}
if !isInteractiveStdin() {
return
}
fmt.Printf("Grant root permissions for `clawgo %s`? (yes/no): ", command)
reader := bufio.NewReader(os.Stdin)
line, _ := reader.ReadString('\n')
answer := strings.ToLower(strings.TrimSpace(line))
if answer != "yes" && answer != "y" {
_ = os.Setenv(envRootPrompted, "1")
_ = os.Setenv(envRootGranted, "0")
return
}
_ = os.Setenv(envRootPrompted, "1")
_ = os.Setenv(envRootGranted, "1")
if os.Geteuid() == 0 {
return
}
exePath, err := os.Executable()
if err != nil {
fmt.Printf("Error resolving executable for sudo re-run: %v\n", err)
os.Exit(1)
}
exePath, _ = filepath.Abs(exePath)
cmdArgs := append([]string{"-E", exePath}, os.Args[1:]...)
cmd := exec.Command("sudo", cmdArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), envRootPrompted+"=1", envRootGranted+"=1")
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
fmt.Printf("Failed to elevate privileges with sudo: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
func shouldPromptGatewayRoot(args []string) bool {
return len(args) == 2 && args[1] == "gateway"
}
func isInteractiveStdin() bool {
info, err := os.Stdin.Stat()
if err != nil {
return false
}
return (info.Mode() & os.ModeCharDevice) != 0
}
func applyMaximumPermissionPolicy(cfg *config.Config) {
cfg.Tools.Shell.Enabled = true
cfg.Tools.Shell.Sandbox.Enabled = false
}
func gatewayInstallServiceCmd() error {
scope, unitPath, err := detectGatewayServiceScopeAndPath()
if err != nil {
return err
}
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("resolve executable path failed: %w", err)
}
exePath, _ = filepath.Abs(exePath)
configPath := getConfigPath()
workDir := filepath.Dir(exePath)
unitContent := buildGatewayUnitContent(scope, exePath, configPath, workDir)
if err := os.MkdirAll(filepath.Dir(unitPath), 0755); err != nil {
return fmt.Errorf("create service directory failed: %w", err)
}
if err := os.WriteFile(unitPath, []byte(unitContent), 0644); err != nil {
return fmt.Errorf("write service unit failed: %w", err)
}
if err := runSystemctl(scope, "daemon-reload"); err != nil {
return err
}
if err := runSystemctl(scope, "enable", gatewayServiceName); err != nil {
return err
}
fmt.Printf("✓ Gateway service registered: %s (%s)\n", gatewayServiceName, scope)
fmt.Printf(" Unit file: %s\n", unitPath)
fmt.Println(" Start service: clawgo gateway start")
fmt.Println(" Restart service: clawgo gateway restart")
fmt.Println(" Stop service: clawgo gateway stop")
return nil
}
func gatewayServiceControlCmd(action string) error {
scope, _, err := detectInstalledGatewayService()
if err != nil {
return err
}
return runSystemctl(scope, action, gatewayServiceName)
}
func detectGatewayServiceScopeAndPath() (string, string, error) {
if runtime.GOOS != "linux" {
return "", "", fmt.Errorf("gateway service registration currently supports Linux systemd only")
}
if strings.ToLower(strings.TrimSpace(os.Getenv("CLAWGO_GATEWAY_SCOPE"))) == "user" {
return userGatewayUnitPath()
}
if strings.ToLower(strings.TrimSpace(os.Getenv("CLAWGO_GATEWAY_SCOPE"))) == "system" {
return "system", "/etc/systemd/system/" + gatewayServiceName, nil
}
if os.Geteuid() == 0 {
return "system", "/etc/systemd/system/" + gatewayServiceName, nil
}
return userGatewayUnitPath()
}
func userGatewayUnitPath() (string, string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", "", fmt.Errorf("resolve user home failed: %w", err)
}
return "user", filepath.Join(home, ".config", "systemd", "user", gatewayServiceName), nil
}
func detectInstalledGatewayService() (string, string, error) {
systemPath := "/etc/systemd/system/" + gatewayServiceName
if info, err := os.Stat(systemPath); err == nil && !info.IsDir() {
return "system", systemPath, nil
}
scope, userPath, err := userGatewayUnitPath()
if err != nil {
return "", "", err
}
if info, err := os.Stat(userPath); err == nil && !info.IsDir() {
return scope, userPath, nil
}
return "", "", fmt.Errorf("gateway service not registered. Run: clawgo gateway")
}
func buildGatewayUnitContent(scope, exePath, configPath, workDir string) string {
quotedExec := fmt.Sprintf("%q gateway run --config %q", exePath, configPath)
installTarget := "default.target"
if scope == "system" {
installTarget = "multi-user.target"
}
home, err := os.UserHomeDir()
if err != nil {
home = filepath.Dir(configPath)
}
return fmt.Sprintf(`[Unit]
Description=ClawGo Gateway
After=network.target
[Service]
Type=simple
WorkingDirectory=%s
ExecStart=%s
Restart=always
RestartSec=3
Environment=CLAWGO_CONFIG=%s
Environment=HOME=%s
[Install]
WantedBy=%s
`, workDir, quotedExec, configPath, home, installTarget)
}
func runSystemctl(scope string, args ...string) error {
cmdArgs := make([]string, 0, len(args)+1)
if scope == "user" {
cmdArgs = append(cmdArgs, "--user")
}
cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("systemctl", cmdArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if scope == "user" {
return fmt.Errorf("systemctl --user %s failed: %w", strings.Join(args, " "), err)
}
return fmt.Errorf("systemctl %s failed: %w", strings.Join(args, " "), err)
}
return nil
}
func buildGatewayRuntime(ctx context.Context, cfg *config.Config, msgBus *bus.MessageBus, cronService *cron.CronService) (*agent.AgentLoop, *channels.Manager, error) {
provider, err := providers.CreateProvider(cfg)
if err != nil {
return nil, nil, fmt.Errorf("create provider: %w", err)
}
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider, cronService)
startupInfo := agentLoop.GetStartupInfo()
toolsInfo := startupInfo["tools"].(map[string]interface{})
skillsInfo := startupInfo["skills"].(map[string]interface{})
fmt.Println("\n📦 Agent Status:")
fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"])
fmt.Printf(" • Skills: %d/%d available\n", skillsInfo["available"], skillsInfo["total"])
logger.InfoCF("agent", "Agent initialized",
map[string]interface{}{
"tools_count": toolsInfo["count"],
"skills_total": skillsInfo["total"],
"skills_available": skillsInfo["available"],
})
channelManager, err := channels.NewManager(cfg, msgBus)
if err != nil {
return nil, nil, fmt.Errorf("create channel manager: %w", err)
}
activeProvider := cfg.Providers.Proxy
if name := strings.TrimSpace(cfg.Agents.Defaults.Proxy); name != "" && name != "proxy" {
if p, ok := cfg.Providers.Proxies[name]; ok {
activeProvider = p
}
}
var transcriber *voice.GroqTranscriber
if activeProvider.APIKey != "" && strings.Contains(activeProvider.APIBase, "groq.com") {
transcriber = voice.NewGroqTranscriber(activeProvider.APIKey)
logger.InfoC("voice", "Groq voice transcription enabled via Proxy config")
}
if transcriber != nil {
if telegramChannel, ok := channelManager.GetChannel("telegram"); ok {
if tc, ok := telegramChannel.(*channels.TelegramChannel); ok {
tc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Telegram channel")
}
}
if discordChannel, ok := channelManager.GetChannel("discord"); ok {
if dc, ok := discordChannel.(*channels.DiscordChannel); ok {
dc.SetTranscriber(transcriber)
logger.InfoC("voice", "Groq transcription attached to Discord channel")
}
}
}
return agentLoop, channelManager, nil
}
func configureCronServiceRuntime(cs *cron.CronService, cfg *config.Config) {
if cs == nil || cfg == nil {
return
}
cs.SetRuntimeOptions(cron.RuntimeOptions{
RunLoopMinSleep: time.Duration(cfg.Cron.MinSleepSec) * time.Second,
RunLoopMaxSleep: time.Duration(cfg.Cron.MaxSleepSec) * time.Second,
RetryBackoffBase: time.Duration(cfg.Cron.RetryBackoffBaseSec) * time.Second,
RetryBackoffMax: time.Duration(cfg.Cron.RetryBackoffMaxSec) * time.Second,
MaxConsecutiveFailureRetries: int64(cfg.Cron.MaxConsecutiveFailureRetries),
MaxWorkers: cfg.Cron.MaxWorkers,
})
}

43
cmd/clawgo/cmd_login.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"bufio"
"fmt"
"os"
"strings"
"clawgo/pkg/config"
)
func loginCmd() {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
fmt.Println("Configuring CLIProxyAPI...")
fmt.Printf("Current Base: %s\n", cfg.Providers.Proxy.APIBase)
fmt.Print("Enter CLIProxyAPI Base URL (e.g. http://localhost:8080/v1): ")
reader := bufio.NewReader(os.Stdin)
line, _ := reader.ReadString('\n')
apiBase := strings.TrimSpace(line)
if apiBase != "" {
cfg.Providers.Proxy.APIBase = apiBase
}
fmt.Print("Enter API Key (optional): ")
fmt.Scanln(&cfg.Providers.Proxy.APIKey)
if err := config.SaveConfig(getConfigPath(), cfg); err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}
fmt.Println("✓ CLIProxyAPI configuration saved.")
}
func configureProvider(cfg *config.Config, provider string) {
// Deprecated: Migrated to CLIProxyAPI logic in loginCmd
}

109
cmd/clawgo/cmd_onboard.go Normal file
View File

@@ -0,0 +1,109 @@
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"clawgo/pkg/config"
)
func onboard() {
configPath := getConfigPath()
if _, err := os.Stat(configPath); err == nil {
fmt.Printf("Config already exists at %s\n", configPath)
fmt.Print("Overwrite? (y/n): ")
var response string
fmt.Scanln(&response)
if response != "y" {
fmt.Println("Aborted.")
return
}
}
cfg := config.DefaultConfig()
if err := config.SaveConfig(configPath, cfg); err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}
workspace := cfg.WorkspacePath()
createWorkspaceTemplates(workspace)
fmt.Printf("%s clawgo is ready!\n", logo)
fmt.Println("\nNext steps:")
fmt.Println(" 1. Configure CLIProxyAPI at", configPath)
fmt.Println(" Ensure CLIProxyAPI is running: https://github.com/router-for-me/CLIProxyAPI")
fmt.Println(" Set providers.<name>.protocol/models; use supports_responses_compact=true only with protocol=responses")
fmt.Println(" 2. Chat: clawgo agent -m \"Hello!\"")
}
func ensureConfigOnboard(configPath string, defaults *config.Config) (string, error) {
if defaults == nil {
return "", fmt.Errorf("defaults is nil")
}
exists := true
if _, err := os.Stat(configPath); os.IsNotExist(err) {
exists = false
} else if err != nil {
return "", err
}
if err := config.SaveConfig(configPath, defaults); err != nil {
return "", err
}
if exists {
return "overwritten", nil
}
return "created", nil
}
func copyEmbeddedToTarget(targetDir string) error {
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create target directory: %w", err)
}
return fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := embeddedFiles.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read embedded file %s: %w", path, err)
}
relPath, err := filepath.Rel("workspace", path)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
}
targetPath := filepath.Join(targetDir, relPath)
if _, statErr := os.Stat(targetPath); statErr == nil {
return nil
} else if !os.IsNotExist(statErr) {
return statErr
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(targetPath), err)
}
if err := os.WriteFile(targetPath, data, 0644); err != nil {
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
}
fmt.Printf(" Created %s\n", relPath)
return nil
})
}
func createWorkspaceTemplates(workspace string) {
err := copyEmbeddedToTarget(workspace)
if err != nil {
fmt.Printf("Error copying workspace templates: %v\n", err)
}
}

273
cmd/clawgo/cmd_skills.go Normal file
View File

@@ -0,0 +1,273 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"clawgo/pkg/config"
"clawgo/pkg/skills"
)
func skillsCmd() {
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)
globalDir := filepath.Dir(getConfigPath())
globalSkillsDir := filepath.Join(globalDir, "skills")
builtinSkillsDir := filepath.Join(globalDir, "clawgo", "skills")
skillsLoader := skills.NewSkillsLoader(workspace, globalSkillsDir, builtinSkillsDir)
switch subcommand {
case "list":
skillsListCmd(skillsLoader)
case "install":
skillsInstallCmd(installer)
case "remove", "uninstall":
if len(os.Args) < 4 {
fmt.Println("Usage: clawgo 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: clawgo skills show <skill-name>")
return
}
skillsShowCmd(skillsLoader, os.Args[3])
default:
fmt.Printf("Unknown skills command: %s\n", subcommand)
skillsHelp()
}
}
func skillsHelp() {
fmt.Println("\nSkills commands:")
fmt.Println(" list List installed skills")
fmt.Println(" install <repo> Install skill from GitHub")
fmt.Println(" install-builtin Install all builtin skills to workspace")
fmt.Println(" list-builtin List available builtin skills")
fmt.Println(" remove <name> Remove installed skill")
fmt.Println(" search Search available skills")
fmt.Println(" show <name> Show skill details")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" clawgo skills list")
fmt.Println(" clawgo skills install YspCoder/clawgo-skills/weather")
fmt.Println(" clawgo skills install-builtin")
fmt.Println(" clawgo skills list-builtin")
fmt.Println(" clawgo skills remove weather")
}
func skillsListCmd(loader *skills.SkillsLoader) {
allSkills := loader.ListSkills()
if len(allSkills) == 0 {
fmt.Println("No skills installed.")
return
}
fmt.Println("\nInstalled Skills:")
fmt.Println("------------------")
for _, skill := range allSkills {
fmt.Printf(" ✓ %s (%s)\n", skill.Name, skill.Source)
if skill.Description != "" {
fmt.Printf(" %s\n", skill.Description)
}
}
}
func skillsInstallCmd(installer *skills.SkillInstaller) {
if len(os.Args) < 4 {
fmt.Println("Usage: clawgo skills install <github-repo>")
fmt.Println("Example: clawgo skills install YspCoder/clawgo-skills/weather")
return
}
repo := os.Args[3]
fmt.Printf("Installing skill from %s...\n", repo)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := installer.InstallFromGitHub(ctx, repo); err != nil {
fmt.Printf("✗ Failed to install skill: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Skill '%s' installed successfully!\n", filepath.Base(repo))
}
func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) {
fmt.Printf("Removing skill '%s'...\n", skillName)
if err := installer.Uninstall(skillName); err != nil {
fmt.Printf("✗ Failed to remove skill: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ Skill '%s' removed successfully!\n", skillName)
}
func skillsInstallBuiltinCmd(workspace string) {
builtinSkillsDir := detectBuiltinSkillsDir(workspace)
workspaceSkillsDir := filepath.Join(workspace, "skills")
fmt.Printf("Copying builtin skills to workspace...\n")
skillsToInstall := []string{"weather", "news", "stock", "calculator"}
for _, skillName := range skillsToInstall {
builtinPath := filepath.Join(builtinSkillsDir, skillName)
workspacePath := filepath.Join(workspaceSkillsDir, skillName)
if _, err := os.Stat(builtinPath); err != nil {
fmt.Printf("⊘ Builtin skill '%s' not found: %v\n", skillName, err)
continue
}
if err := os.MkdirAll(workspacePath, 0755); err != nil {
fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err)
continue
}
if err := copyDirectory(builtinPath, workspacePath); err != nil {
fmt.Printf("✗ Failed to copy %s: %v\n", skillName, err)
}
}
fmt.Println("\n✓ All builtin skills installed!")
fmt.Println("Now you can use them in your workspace.")
}
func skillsListBuiltinCmd() {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
builtinSkillsDir := detectBuiltinSkillsDir(cfg.WorkspacePath())
fmt.Println("\nAvailable Builtin Skills:")
fmt.Println("-----------------------")
entries, err := os.ReadDir(builtinSkillsDir)
if err != nil {
fmt.Printf("Error reading builtin skills: %v\n", err)
return
}
if len(entries) == 0 {
fmt.Println("No builtin skills available.")
return
}
for _, entry := range entries {
if entry.IsDir() {
skillName := entry.Name()
skillFile := filepath.Join(builtinSkillsDir, skillName, "SKILL.md")
description := "No description"
if _, err := os.Stat(skillFile); err == nil {
data, err := os.ReadFile(skillFile)
if err == nil {
content := string(data)
if idx := strings.Index(content, "\n"); idx > 0 {
firstLine := content[:idx]
if strings.Contains(firstLine, "description:") {
descLine := strings.Index(content[idx:], "\n")
if descLine > 0 {
description = strings.TrimSpace(content[idx+descLine : idx+descLine])
}
}
}
}
}
status := "✓"
fmt.Printf(" %s %s\n", status, entry.Name())
if description != "" {
fmt.Printf(" %s\n", description)
}
}
}
}
func detectBuiltinSkillsDir(workspace string) string {
candidates := []string{
filepath.Join(".", "skills"),
filepath.Join(filepath.Dir(workspace), "clawgo", "skills"),
filepath.Join(config.GetConfigDir(), "clawgo", "skills"),
}
for _, dir := range candidates {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
return dir
}
}
return filepath.Join(".", "skills")
}
func skillsSearchCmd(installer *skills.SkillInstaller) {
fmt.Println("Searching for available skills...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
availableSkills, err := installer.ListAvailableSkills(ctx)
if err != nil {
fmt.Printf("✗ Failed to fetch skills list: %v\n", err)
return
}
if len(availableSkills) == 0 {
fmt.Println("No skills available.")
return
}
fmt.Printf("\nAvailable Skills (%d):\n", len(availableSkills))
fmt.Println("--------------------")
for _, skill := range availableSkills {
fmt.Printf(" 📦 %s\n", skill.Name)
fmt.Printf(" %s\n", skill.Description)
fmt.Printf(" Repo: %s\n", skill.Repository)
if skill.Author != "" {
fmt.Printf(" Author: %s\n", skill.Author)
}
if len(skill.Tags) > 0 {
fmt.Printf(" Tags: %v\n", skill.Tags)
}
fmt.Println()
}
}
func skillsShowCmd(loader *skills.SkillsLoader, skillName string) {
content, ok := loader.LoadSkill(skillName)
if !ok {
fmt.Printf("✗ Skill '%s' not found\n", skillName)
return
}
fmt.Printf("\n📦 Skill: %s\n", skillName)
fmt.Println("----------------------")
fmt.Println(content)
}

68
cmd/clawgo/cmd_status.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"fmt"
"os"
"strings"
"clawgo/pkg/providers"
)
func statusCmd() {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
configPath := getConfigPath()
fmt.Printf("%s clawgo Status\n\n", logo)
if _, err := os.Stat(configPath); err == nil {
fmt.Println("Config:", configPath, "✓")
} else {
fmt.Println("Config:", configPath, "✗")
}
workspace := cfg.WorkspacePath()
if _, err := os.Stat(workspace); err == nil {
fmt.Println("Workspace:", workspace, "✓")
} else {
fmt.Println("Workspace:", workspace, "✗")
}
if _, err := os.Stat(configPath); err == nil {
activeProvider := cfg.Providers.Proxy
activeProxyName := "proxy"
if name := strings.TrimSpace(cfg.Agents.Defaults.Proxy); name != "" && name != "proxy" {
if p, ok := cfg.Providers.Proxies[name]; ok {
activeProvider = p
activeProxyName = name
}
}
activeModel := ""
for _, m := range activeProvider.Models {
if s := strings.TrimSpace(m); s != "" {
activeModel = s
break
}
}
fmt.Printf("Model: %s\n", activeModel)
fmt.Printf("Proxy: %s\n", activeProxyName)
fmt.Printf("CLIProxyAPI Base: %s\n", cfg.Providers.Proxy.APIBase)
fmt.Printf("Supports /v1/responses/compact: %v\n", providers.ProviderSupportsResponsesCompact(cfg, activeProxyName))
hasKey := cfg.Providers.Proxy.APIKey != ""
status := "not set"
if hasKey {
status = "✓"
}
fmt.Printf("CLIProxyAPI Key: %s\n", status)
fmt.Printf("Logging: %v\n", cfg.Logging.Enabled)
if cfg.Logging.Enabled {
fmt.Printf("Log File: %s\n", cfg.LogFilePath())
fmt.Printf("Log Max Size: %d MB\n", cfg.Logging.MaxSizeMB)
fmt.Printf("Log Retention: %d days\n", cfg.Logging.RetentionDays)
}
}
}

View File

@@ -0,0 +1,71 @@
package main
import (
"fmt"
"os"
"path/filepath"
)
func uninstallCmd() {
purge := false
removeBin := false
for _, arg := range os.Args[2:] {
switch arg {
case "--purge":
purge = true
case "--remove-bin":
removeBin = true
}
}
if err := uninstallGatewayService(); err != nil {
fmt.Printf("Gateway service uninstall warning: %v\n", err)
} else {
fmt.Println("✓ Gateway service uninstalled")
}
pidPath := filepath.Join(filepath.Dir(getConfigPath()), "gateway.pid")
_ = os.Remove(pidPath)
if purge {
configDir := filepath.Dir(getConfigPath())
if err := os.RemoveAll(configDir); err != nil {
fmt.Printf("Failed to remove config directory %s: %v\n", configDir, err)
os.Exit(1)
}
fmt.Printf("✓ Removed config/workspace directory: %s\n", configDir)
}
if removeBin {
exePath, err := os.Executable()
if err != nil {
fmt.Printf("Failed to resolve executable path: %v\n", err)
os.Exit(1)
}
if err := os.Remove(exePath); err != nil {
fmt.Printf("Failed to remove executable %s: %v\n", exePath, err)
os.Exit(1)
}
fmt.Printf("✓ Removed executable: %s\n", exePath)
}
}
func uninstallGatewayService() error {
scope, unitPath, err := detectInstalledGatewayService()
if err != nil {
return nil
}
_ = runSystemctl(scope, "stop", gatewayServiceName)
_ = runSystemctl(scope, "disable", gatewayServiceName)
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove unit file failed: %w", err)
}
if err := runSystemctl(scope, "daemon-reload"); err != nil {
return err
}
return nil
}

File diff suppressed because it is too large Load Diff