• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smallnest / goclaw / 22518818095

28 Feb 2026 10:18AM UTC coverage: 8.86% (-0.02%) from 8.879%
22518818095

push

github

smallnest
fix cron

12 of 538 new or added lines in 30 files covered. (2.23%)

9 existing lines in 8 files now uncovered.

2868 of 32371 relevant lines covered (8.86%)

0.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

21.98
/cli/cron_cli.go
1
package cli
2

3
import (
4
        "encoding/json"
5
        "fmt"
6
        "os"
7
        "time"
8

9
        "github.com/smallnest/goclaw/config"
10
        "github.com/spf13/cobra"
11
)
12

13
var cronCmd = &cobra.Command{
14
        Use:   "cron",
15
        Short: "Scheduled jobs management (via Gateway)",
16
}
17

18
func init() {
1✔
19
        // Register cron command to rootCmd
1✔
20
        rootCmd.AddCommand(cronCmd)
1✔
21
}
1✔
22

23
var cronStatusCmd = &cobra.Command{
24
        Use:   "status",
25
        Short: "Show scheduler status",
26
        Run:   runCronStatus,
27
}
28

29
var cronListCmd = &cobra.Command{
30
        Use:   "list",
31
        Short: "List all jobs",
32
        Run:   runCronList,
33
}
34

35
var cronAddCmd = &cobra.Command{
36
        Use:   "add",
37
        Short: "Add a new scheduled job",
38
        Run:   runCronAdd,
39
}
40

41
var cronEditCmd = &cobra.Command{
42
        Use:   "edit <id>",
43
        Short: "Edit an existing job",
44
        Args:  cobra.ExactArgs(1),
45
        Run:   runCronEdit,
46
}
47

48
var cronRmCmd = &cobra.Command{
49
        Use:   "rm <id>",
50
        Short: "Delete a job",
51
        Args:  cobra.ExactArgs(1),
52
        Run:   runCronRm,
53
}
54

55
var cronEnableCmd = &cobra.Command{
56
        Use:   "enable <id>",
57
        Short: "Enable a job",
58
        Args:  cobra.ExactArgs(1),
59
        Run:   runCronEnable,
60
}
61

62
var cronDisableCmd = &cobra.Command{
63
        Use:   "disable <id>",
64
        Short: "Disable a job",
65
        Args:  cobra.ExactArgs(1),
66
        Run:   runCronDisable,
67
}
68

69
var cronRunsCmd = &cobra.Command{
70
        Use:   "runs",
71
        Short: "View job run history",
72
        Run:   runCronRuns,
73
}
74

75
var cronRunCmd = &cobra.Command{
76
        Use:   "run <id>",
77
        Short: "Run a job immediately",
78
        Args:  cobra.ExactArgs(1),
79
        Run:   runCronRun,
80
}
81

82
// Cron flags
83
var (
84
        cronStatusJSON bool
85
        cronListAll    bool
86
        cronListJSON   bool
87

88
        // add flags
89
        cronAddName        string
90
        cronAddAt          string
91
        cronAddEvery       string
92
        cronAddCron        string
93
        cronAddMessage     string
94
        cronAddSystemEvent string
95
        cronAddWebhook     string
96
        cronAddSession     string
97

98
        // run flags
99
        cronRunForce bool // force run even if disabled
100

101
        // runs flags
102
        cronRunsID    string
103
        cronRunsLimit int
104
        cronRunsJSON  bool
105
)
106

107
func init() {
1✔
108
        // Status command flags
1✔
109
        cronStatusCmd.Flags().BoolVar(&cronStatusJSON, "json", false, "Output JSON")
1✔
110

1✔
111
        // List command flags
1✔
112
        cronListCmd.Flags().BoolVarP(&cronListAll, "all", "a", false, "Include disabled jobs")
1✔
113
        cronListCmd.Flags().BoolVar(&cronListJSON, "json", false, "Output JSON")
1✔
114

1✔
115
        // Add command flags
1✔
116
        cronAddCmd.Flags().StringVarP(&cronAddName, "name", "n", "", "Job name (required)")
1✔
117
        cronAddCmd.Flags().StringVar(&cronAddAt, "at", "", "Run at specific time (RFC3339 format)")
1✔
118
        cronAddCmd.Flags().StringVar(&cronAddEvery, "every", "", "Run every interval (e.g., 30s, 5m, 2h, 1d)")
1✔
119
        cronAddCmd.Flags().StringVar(&cronAddCron, "cron", "", "Cron expression (e.g., '0 8 * * *')")
1✔
120
        cronAddCmd.Flags().StringVarP(&cronAddMessage, "message", "m", "", "Message to send (agent-turn payload)")
1✔
121
        cronAddCmd.Flags().StringVar(&cronAddSystemEvent, "system-event", "", "System event type (system-event payload)")
1✔
122
        cronAddCmd.Flags().StringVar(&cronAddWebhook, "webhook", "", "Webhook URL for delivery")
1✔
123
        cronAddCmd.Flags().StringVar(&cronAddSession, "session", "main", "Session target (main or isolated)")
1✔
124
        if err := cronAddCmd.MarkFlagRequired("name"); err != nil {
1✔
NEW
125
                panic(err)
×
126
        }
127

128
        // Run command flags
129
        cronRunCmd.Flags().BoolVarP(&cronRunForce, "force", "f", false, "Force run even if disabled")
1✔
130

1✔
131
        // Runs command flags
1✔
132
        cronRunsCmd.Flags().StringVar(&cronRunsID, "id", "", "Job ID (optional)")
1✔
133
        cronRunsCmd.Flags().IntVar(&cronRunsLimit, "limit", 50, "Max number of runs to show")
1✔
134
        cronRunsCmd.Flags().BoolVar(&cronRunsJSON, "json", false, "Output JSON")
1✔
135

1✔
136
        // Register subcommands
1✔
137
        cronCmd.AddCommand(cronStatusCmd)
1✔
138
        cronCmd.AddCommand(cronListCmd)
1✔
139
        cronCmd.AddCommand(cronAddCmd)
1✔
140
        cronCmd.AddCommand(cronEditCmd)
1✔
141
        cronCmd.AddCommand(cronRmCmd)
1✔
142
        cronCmd.AddCommand(cronEnableCmd)
1✔
143
        cronCmd.AddCommand(cronDisableCmd)
1✔
144
        cronCmd.AddCommand(cronRunsCmd)
1✔
145
        cronCmd.AddCommand(cronRunCmd)
1✔
146
}
147

148
func runCronStatus(cmd *cobra.Command, args []string) {
×
149
        cfg, err := config.Load("")
×
150
        if err != nil {
×
151
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
152
                os.Exit(1)
×
153
        }
×
154

155
        result, err := callGatewayRPC(cfg, "cron.status", map[string]interface{}{})
×
156
        if err != nil {
×
157
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
158
                os.Exit(1)
×
159
        }
×
160

161
        if cronStatusJSON {
×
162
                printJSON(result)
×
163
                return
×
164
        }
×
165

166
        status := result.(map[string]interface{})
×
167

×
168
        fmt.Println("Cron Scheduler Status")
×
169
        fmt.Println("======================")
×
170
        fmt.Printf("Running: %v\n", status["running"])
×
171
        fmt.Printf("Job Count: %v\n", status["job_count"])
×
172
        if jobCount, ok := status["job_count"].(map[string]interface{}); ok {
×
173
                if enabled, ok := jobCount["enabled"]; ok {
×
174
                        fmt.Printf("  Enabled: %v\n", enabled)
×
175
                }
×
176
                if disabled, ok := jobCount["disabled"]; ok {
×
177
                        fmt.Printf("  Disabled: %v\n", disabled)
×
178
                }
×
179
        }
180
}
181

182
func runCronList(cmd *cobra.Command, args []string) {
×
183
        cfg, err := config.Load("")
×
184
        if err != nil {
×
185
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
186
                os.Exit(1)
×
187
        }
×
188
        result, err := callGatewayRPC(cfg, "cron.list", map[string]interface{}{
×
189
                "include_disabled": cronListAll,
×
190
        })
×
191
        if err != nil {
×
192
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
193
                os.Exit(1)
×
194
        }
×
195

196
        if cronListJSON {
×
197
                printJSON(result)
×
198
                return
×
199
        }
×
200

201
        data := result.(map[string]interface{})
×
202
        jobs, _ := data["jobs"].([]interface{})
×
203

×
204
        if len(jobs) == 0 {
×
205
                fmt.Println("No jobs found.")
×
206
                return
×
207
        }
×
208

209
        fmt.Printf("Found %d job(s):\n\n", len(jobs))
×
210
        for _, j := range jobs {
×
211
                job := j.(map[string]interface{})
×
212
                printJob(job)
×
213
                fmt.Println()
×
214
        }
×
215
}
216

217
func runCronAdd(cmd *cobra.Command, args []string) {
×
218
        cfg, err := config.Load("")
×
219
        if err != nil {
×
220
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
221
                os.Exit(1)
×
222
        }
×
223

224
        // Build schedule
225
        schedule := make(map[string]interface{})
×
226
        if cronAddAt != "" {
×
227
                schedule["type"] = "at"
×
228
                schedule["at"] = cronAddAt
×
229
        } else if cronAddEvery != "" {
×
230
                schedule["type"] = "every"
×
231
                schedule["every"] = cronAddEvery
×
232
        } else if cronAddCron != "" {
×
233
                schedule["type"] = "cron"
×
234
                schedule["cron"] = cronAddCron
×
235
        } else {
×
236
                fmt.Fprintf(os.Stderr, "Error: must specify one of --at, --every, or --cron\n")
×
237
                os.Exit(1)
×
238
        }
×
239

240
        // Build payload
241
        payload := make(map[string]interface{})
×
242
        if cronAddMessage != "" {
×
243
                payload["type"] = "agent-turn"
×
244
                payload["message"] = cronAddMessage
×
245
        } else if cronAddSystemEvent != "" {
×
246
                payload["type"] = "system-event"
×
247
                payload["system_event_type"] = cronAddSystemEvent
×
248
        } else {
×
249
                fmt.Fprintf(os.Stderr, "Error: must specify --message or --system-event\n")
×
250
                os.Exit(1)
×
251
        }
×
252

253
        // Build job params
254
        params := map[string]interface{}{
×
255
                "name":           cronAddName,
×
256
                "schedule":       schedule,
×
257
                "payload":        payload,
×
258
                "session_target": cronAddSession,
×
259
        }
×
260

×
261
        if cronAddWebhook != "" {
×
262
                delivery := map[string]interface{}{
×
263
                        "mode":        "webhook",
×
264
                        "webhook_url": cronAddWebhook,
×
265
                }
×
266
                params["delivery"] = delivery
×
267
        }
×
268

269
        result, err := callGatewayRPC(cfg, "cron.add", params)
×
270
        if err != nil {
×
271
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
272
                os.Exit(1)
×
273
        }
×
274

275
        job := result.(map[string]interface{})
×
276
        fmt.Printf("Job '%s' added with ID: %s\n", job["name"], job["id"])
×
277
}
278

279
func runCronEdit(cmd *cobra.Command, args []string) {
×
280
        fmt.Println("Edit command is not yet implemented via CLI.")
×
281
        fmt.Println("Use cron update RPC method directly or implement edit functionality.")
×
282
}
×
283

284
func runCronRm(cmd *cobra.Command, args []string) {
×
285
        id := args[0]
×
286
        cfg, err := config.Load("")
×
287
        if err != nil {
×
288
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
289
                os.Exit(1)
×
290
        }
×
291

292
        result, err := callGatewayRPC(cfg, "cron.remove", map[string]interface{}{
×
293
                "id": id,
×
294
        })
×
295
        if err != nil {
×
296
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
297
                os.Exit(1)
×
298
        }
×
299

300
        res := result.(map[string]interface{})
×
301
        fmt.Printf("Job '%s' %s\n", id, res["status"])
×
302
}
303

304
func runCronEnable(cmd *cobra.Command, args []string) {
×
305
        id := args[0]
×
306
        cfg, err := config.Load("")
×
307
        if err != nil {
×
308
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
309
                os.Exit(1)
×
310
        }
×
311

312
        _, err = callGatewayRPC(cfg, "cron.update", map[string]interface{}{
×
313
                "id": id,
×
314
                "patch": map[string]interface{}{
×
315
                        "enabled": true,
×
316
                },
×
317
        })
×
318
        if err != nil {
×
319
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
320
                os.Exit(1)
×
321
        }
×
322

323
        fmt.Printf("Job '%s' enabled\n", id)
×
324
}
325

326
func runCronDisable(cmd *cobra.Command, args []string) {
×
327
        id := args[0]
×
328
        cfg, err := config.Load("")
×
329
        if err != nil {
×
330
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
331
                os.Exit(1)
×
332
        }
×
333

334
        _, err = callGatewayRPC(cfg, "cron.update", map[string]interface{}{
×
335
                "id": id,
×
336
                "patch": map[string]interface{}{
×
337
                        "enabled": false,
×
338
                },
×
339
        })
×
340
        if err != nil {
×
341
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
342
                os.Exit(1)
×
343
        }
×
344

345
        fmt.Printf("Job '%s' disabled\n", id)
×
346
}
347

348
func runCronRuns(cmd *cobra.Command, args []string) {
×
349
        cfg, err := config.Load("")
×
350
        if err != nil {
×
351
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
352
                os.Exit(1)
×
353
        }
×
354

355
        // Use provided id or show usage note
356
        if cronRunsID == "" && cmd.Flags().Changed("id") {
×
357
                fmt.Fprintf(os.Stderr, "Error: --id is required\n")
×
358
                os.Exit(1)
×
359
        }
×
360

361
        if cronRunsID == "" {
×
362
                fmt.Println("Usage: goclaw cron runs --id <job-id>")
×
363
                fmt.Println("       goclaw cron runs --id <job-id> --limit 100")
×
364
                return
×
365
        }
×
366

367
        result, err := callGatewayRPC(cfg, "cron.runs", map[string]interface{}{
×
368
                "id":    cronRunsID,
×
369
                "limit": cronRunsLimit,
×
370
        })
×
371
        if err != nil {
×
372
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
373
                os.Exit(1)
×
374
        }
×
375

376
        if cronRunsJSON {
×
377
                printJSON(result)
×
378
                return
×
379
        }
×
380

381
        data := result.(map[string]interface{})
×
382
        runs := data["runs"].([]interface{})
×
383

×
384
        fmt.Printf("Run History for Job '%s' (last %d runs):\n\n", cronRunsID, len(runs))
×
385

×
386
        for i, r := range runs {
×
387
                run := r.(map[string]interface{})
×
388
                fmt.Printf("  %d. %s\n", i+1, formatTimeStr(run["started_at"]))
×
389
                fmt.Printf("     Status: %s\n", run["status"])
×
390
                if dur, ok := run["duration"].(string); ok && dur != "" {
×
391
                        fmt.Printf("     Duration: %s\n", dur)
×
392
                }
×
393
                if err, ok := run["error"].(string); ok && err != "" {
×
394
                        fmt.Printf("     Error: %s\n", err)
×
395
                }
×
396
                fmt.Println()
×
397
        }
398
}
399

400
func runCronRun(cmd *cobra.Command, args []string) {
×
401
        id := args[0]
×
402
        cfg, err := config.Load("")
×
403
        if err != nil {
×
404
                fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
×
405
                os.Exit(1)
×
406
        }
×
407

408
        mode := "normal"
×
409
        if cronRunForce {
×
410
                mode = "force"
×
411
        }
×
412

413
        result, err := callGatewayRPC(cfg, "cron.run", map[string]interface{}{
×
414
                "id":   id,
×
415
                "mode": mode,
×
416
        })
×
417
        if err != nil {
×
418
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
419
                os.Exit(1)
×
420
        }
×
421

422
        res := result.(map[string]interface{})
×
423
        fmt.Printf("Job '%s' run initiated\n", id)
×
424
        fmt.Printf("Status: %s\n", res["status"])
×
425

×
426
        // Suggest viewing run logs
×
427
        fmt.Printf("\nView run logs: ./goclaw cron runs --id %s\n", id)
×
428
}
429

430
func printJob(job map[string]interface{}) {
1✔
431
        fmt.Printf("ID: %s\n", job["id"])
1✔
432
        fmt.Printf("Name: %s\n", job["name"])
1✔
433
        enabled := "-"
1✔
434
        if state, ok := job["state"].(map[string]interface{}); ok {
1✔
435
                if v, exists := state["enabled"]; exists {
×
436
                        enabled = fmt.Sprintf("%v", v)
×
437
                }
×
438
        }
439
        fmt.Printf("Enabled: %s\n", enabled)
1✔
440
        fmt.Printf("Schedule: %s\n", formatScheduleFromMap(job))
1✔
441

1✔
442
        if payload, ok := job["payload"].(map[string]interface{}); ok {
1✔
443
                ptype := payload["type"]
×
444
                if ptype == "agent-turn" {
×
445
                        fmt.Printf("Payload: message: %s\n", payload["message"])
×
446
                } else if ptype == "system-event" {
×
447
                        fmt.Printf("Payload: event: %s\n", payload["system_event_type"])
×
448
                }
×
449
        }
450

451
        if delivery, ok := job["delivery"].(map[string]interface{}); ok && delivery != nil {
1✔
452
                fmt.Printf("Delivery: %s", delivery["mode"])
×
453
                if webhook, ok := delivery["webhook_url"].(string); ok && webhook != "" {
×
454
                        fmt.Printf(" (%s)", webhook)
×
455
                }
×
456
                fmt.Println()
×
457
        }
458

459
        fmt.Printf("Created: %s\n", formatTimeStr(job["created_at"]))
1✔
460

1✔
461
        if state, ok := job["state"].(map[string]interface{}); ok {
1✔
462
                if nextRun, ok := state["next_run_at"]; ok && nextRun != nil {
×
463
                        fmt.Printf("Next Run: %s\n", formatTimeStr(nextRun))
×
464
                }
×
465
                if lastRun, ok := state["last_run_at"]; ok && lastRun != nil {
×
466
                        fmt.Printf("Last Run: %s\n", formatTimeStr(lastRun))
×
467
                }
×
468
        }
469
}
470

471
func formatScheduleFromMap(job map[string]interface{}) string {
4✔
472
        schedule, ok := job["schedule"].(map[string]interface{})
4✔
473
        if !ok {
4✔
474
                return "unknown"
×
475
        }
×
476

477
        typ, _ := schedule["type"].(string)
4✔
478
        switch typ {
4✔
479
        case "at":
×
480
                if at, ok := schedule["at"]; ok && at != nil {
×
481
                        return "at " + formatTimeStr(at)
×
482
                }
×
483
                if atISO, ok := schedule["at_iso"]; ok && atISO != nil {
×
484
                        return "at " + formatTimeStr(atISO)
×
485
                }
×
486
                return "at <invalid>"
×
487
        case "every":
2✔
488
                if every, ok := schedule["every"].(string); ok && every != "" {
3✔
489
                        return "every " + every
1✔
490
                }
1✔
491
                if everyMs, ok := schedule["every_duration_ms"]; ok && everyMs != nil {
2✔
492
                        if ms, ok := toInt64(everyMs); ok && ms > 0 {
2✔
493
                                return "every " + (time.Duration(ms) * time.Millisecond).String()
1✔
494
                        }
1✔
495
                }
496
                return "every <invalid>"
×
497
        case "cron":
2✔
498
                if cronExpr, ok := schedule["cron_expression"].(string); ok && cronExpr != "" {
3✔
499
                        return cronExpr
1✔
500
                }
1✔
501
                if cronExpr, ok := schedule["cron"].(string); ok && cronExpr != "" {
1✔
502
                        return cronExpr
×
503
                }
×
504
                return "<invalid cron>"
1✔
505
        default:
×
506
                return "unknown"
×
507
        }
508
}
509

510
func toInt64(v interface{}) (int64, bool) {
1✔
511
        switch n := v.(type) {
1✔
512
        case int:
×
513
                return int64(n), true
×
514
        case int32:
×
515
                return int64(n), true
×
516
        case int64:
×
517
                return n, true
×
518
        case float32:
×
519
                return int64(n), true
×
520
        case float64:
1✔
521
                return int64(n), true
1✔
522
        default:
×
523
                return 0, false
×
524
        }
525
}
526

527
func formatTimeStr(t interface{}) string {
1✔
528
        if t == nil {
1✔
529
                return "-"
×
530
        }
×
531
        if s, ok := t.(string); ok {
2✔
532
                // Try to parse and format
1✔
533
                if pt, err := time.Parse(time.RFC3339Nano, s); err == nil {
2✔
534
                        return pt.Format("2006-01-02 15:04:05")
1✔
535
                }
1✔
536
                return s
×
537
        }
538
        return fmt.Sprintf("%v", t)
×
539
}
540

541
func printJSON(v interface{}) {
×
542
        data, err := json.MarshalIndent(v, "", "  ")
×
543
        if err != nil {
×
544
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
545
                os.Exit(1)
×
546
        }
×
547
        fmt.Println(string(data))
×
548
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc