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

smallnest / goclaw / 22514309419

28 Feb 2026 05:29AM UTC coverage: 8.888% (+4.6%) from 4.278%
22514309419

push

github

smallnest
replace with custiomized cli

0 of 492 new or added lines in 4 files covered. (0.0%)

3864 existing lines in 35 files now uncovered.

2864 of 32222 relevant lines covered (8.89%)

0.52 hits per line

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

22.67
/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
        cronAddCmd.MarkFlagRequired("name")
1✔
125

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

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

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

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

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

159
        if cronStatusJSON {
×
160
                printJSON(result)
×
161
                return
×
UNCOV
162
        }
×
163

164
        status := result.(map[string]interface{})
×
165

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

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

194
        if cronListJSON {
×
195
                printJSON(result)
×
196
                return
×
UNCOV
197
        }
×
198

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

374
        if cronRunsJSON {
×
375
                printJSON(result)
×
376
                return
×
377
        }
×
378

379
        data := result.(map[string]interface{})
×
UNCOV
380
        runs := data["runs"].([]interface{})
×
UNCOV
381

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

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

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

406
        mode := "normal"
×
407
        if cronRunForce {
×
408
                mode = "force"
×
UNCOV
409
        }
×
410

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

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

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

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

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

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

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

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

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

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

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

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

539
func printJSON(v interface{}) {
×
540
        data, err := json.MarshalIndent(v, "", "  ")
×
541
        if err != nil {
×
UNCOV
542
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
×
543
                os.Exit(1)
×
544
        }
×
545
        fmt.Println(string(data))
×
546
}
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