Files
goclaw/cmd/cron_cmd.go
T
Viet Tran 7b7e9a4248 Add user_id and agent_id filtering to cron jobs with token usage tracking and UUID-based agent resolution
Extend CronJob model with UserID field for multi-tenant isolation. Add agentID and userID filter parameters to ListJobs across all store implementations (file, pg). Support agent lookup by UUID in resolver for cron jobs that store agent_id as UUID. Change cron job handler signature to return CronJobResult struct with content, token usage (input/output tokens), and duration. Track execution metrics
2026-02-22 21:19:29 +07:00

201 lines
4.9 KiB
Go

package cmd
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/cron"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/internal/store/file"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
func cronCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "cron",
Short: "Manage scheduled cron jobs",
}
cmd.AddCommand(cronListCmd())
cmd.AddCommand(cronDeleteCmd())
cmd.AddCommand(cronToggleCmd())
return cmd
}
func cronListCmd() *cobra.Command {
var jsonOutput bool
var showDisabled bool
cmd := &cobra.Command{
Use: "list",
Short: "List all cron jobs",
Run: func(cmd *cobra.Command, args []string) {
if isManagedMode() {
cronListRPC(showDisabled, jsonOutput)
return
}
svc := loadCronStore()
jobs := svc.ListJobs(showDisabled, "", "")
printCronJobs(jobs, jsonOutput)
},
}
cmd.Flags().BoolVar(&jsonOutput, "json", false, "output as JSON")
cmd.Flags().BoolVar(&showDisabled, "all", false, "include disabled jobs")
return cmd
}
func cronDeleteCmd() *cobra.Command {
return &cobra.Command{
Use: "delete [jobId]",
Short: "Delete a cron job",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if isManagedMode() {
cronDeleteRPC(args[0])
return
}
svc := loadCronStore()
if err := svc.RemoveJob(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
fmt.Printf("Deleted job %s\n", args[0])
},
}
}
func cronToggleCmd() *cobra.Command {
return &cobra.Command{
Use: "toggle [jobId] [true|false]",
Short: "Enable or disable a cron job",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
enabled := args[1] == "true" || args[1] == "1" || args[1] == "on"
if isManagedMode() {
cronToggleRPC(args[0], enabled)
return
}
svc := loadCronStore()
if err := svc.EnableJob(args[0], enabled); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
fmt.Printf("Job %s enabled=%v\n", args[0], enabled)
},
}
}
// --- RPC implementations (managed mode) ---
func cronListRPC(showDisabled, jsonOutput bool) {
requireGatewayForManaged()
params, _ := json.Marshal(map[string]interface{}{"includeDisabled": showDisabled})
resp, err := gatewayRPC(protocol.MethodCronList, params)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if !resp.OK {
fmt.Fprintf(os.Stderr, "Failed: %s\n", resp.Error.Message)
os.Exit(1)
}
raw, _ := json.Marshal(resp.Payload)
var result struct {
Jobs []store.CronJob `json:"jobs"`
}
if err := json.Unmarshal(raw, &result); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err)
os.Exit(1)
}
printCronJobs(result.Jobs, jsonOutput)
}
func cronDeleteRPC(jobID string) {
requireGatewayForManaged()
params, _ := json.Marshal(map[string]string{"jobId": jobID})
resp, err := gatewayRPC(protocol.MethodCronDelete, params)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if !resp.OK {
fmt.Fprintf(os.Stderr, "Failed: %s\n", resp.Error.Message)
os.Exit(1)
}
fmt.Printf("Deleted job %s\n", jobID)
}
func cronToggleRPC(jobID string, enabled bool) {
requireGatewayForManaged()
params, _ := json.Marshal(map[string]interface{}{"jobId": jobID, "enabled": enabled})
resp, err := gatewayRPC(protocol.MethodCronToggle, params)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if !resp.OK {
fmt.Fprintf(os.Stderr, "Failed: %s\n", resp.Error.Message)
os.Exit(1)
}
fmt.Printf("Job %s enabled=%v\n", jobID, enabled)
}
// --- Shared display ---
func printCronJobs(jobs []store.CronJob, jsonOutput bool) {
if jsonOutput {
data, _ := json.MarshalIndent(jobs, "", " ")
fmt.Println(string(data))
return
}
if len(jobs) == 0 {
fmt.Println("No cron jobs configured.")
return
}
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(tw, "ID\tNAME\tENABLED\tSCHEDULE\tLAST RUN\n")
for _, j := range jobs {
schedule := j.Schedule.Kind
if j.Schedule.Expr != "" {
schedule = j.Schedule.Expr
} else if j.Schedule.EveryMS != nil {
d := time.Duration(*j.Schedule.EveryMS) * time.Millisecond
schedule = "every " + d.String()
}
lastRun := "never"
if j.State.LastRunAtMS != nil {
lastRun = time.UnixMilli(*j.State.LastRunAtMS).Format(time.DateTime)
}
idShort := j.ID
if len(idShort) > 8 {
idShort = idShort[:8]
}
fmt.Fprintf(tw, "%s\t%s\t%v\t%s\t%s\n",
idShort, j.Name, j.Enabled, schedule, lastRun)
}
tw.Flush()
}
// --- File-based helpers (standalone mode) ---
func loadCronStore() store.CronStore {
dataDir := config.ExpandHome("~/.goclaw/data")
storePath := filepath.Join(dataDir, "cron", "jobs.json")
return file.NewFileCronStore(cron.NewService(storePath, nil))
}