From 78064da29a49a7c3f51d4275df58458116f96474 Mon Sep 17 00:00:00 2001 From: muava12 Date: Wed, 25 Feb 2026 10:27:58 +0800 Subject: [PATCH] feat(prayer): implement ai-independent prayer scheduling Refactor prayer notification system to remove AI dependency for daily scheduling. Changes include: - Added '--at', '--command', and '--delete-after' flags to 'picoclaw cron add' CLI. - Rewrote 'prayer_notify.sh auto_schedule' to use CLI instead of direct JSON writes. - Implemented Indonesia city-to-timezone mapping (WIB/WITA/WIT) for accurate scheduling. - Added cron store reload in 'pkg/tools/cron.go' after command execution. - Updated 'SKILL.md' with new setup instructions. --- cmd/picoclaw/cmd_cron.go | 51 ++++- pkg/tools/cron.go | 7 +- workspace/skills/prayer-times/SKILL.md | 67 +++--- .../prayer-times/scripts/prayer_notify.sh | 207 +++++++++++++++++- 4 files changed, 275 insertions(+), 57 deletions(-) diff --git a/cmd/picoclaw/cmd_cron.go b/cmd/picoclaw/cmd_cron.go index 8c42bde06..2f6d08c00 100644 --- a/cmd/picoclaw/cmd_cron.go +++ b/cmd/picoclaw/cmd_cron.go @@ -62,10 +62,13 @@ func cronHelp() { fmt.Println(" -n, --name Job name") fmt.Println(" -m, --message Message for agent") fmt.Println(" -e, --every Run every N seconds") + fmt.Println(" -a, --at Run once in N seconds from now") fmt.Println(" -c, --cron Cron expression (e.g. '0 9 * * *')") - fmt.Println(" -d, --deliver Deliver response to channel") + fmt.Println(" --command Shell command to execute") + fmt.Println(" -d, --deliver Deliver response to channel") fmt.Println(" --to Recipient for delivery") fmt.Println(" --channel Channel for delivery") + fmt.Println(" --delete-after Delete job after first run (default for --at)") } func cronListCmd(storePath string) { @@ -111,8 +114,11 @@ func cronAddCmd(storePath string) { name := "" message := "" var everySec *int64 + var atSec *int64 cronExpr := "" + command := "" deliver := false + deleteAfter := false channel := "" to := "" @@ -136,13 +142,27 @@ func cronAddCmd(storePath string) { everySec = &sec i++ } + case "-a", "--at": + if i+1 < len(args) { + var sec int64 + fmt.Sscanf(args[i+1], "%d", &sec) + atSec = &sec + i++ + } case "-c", "--cron": if i+1 < len(args) { cronExpr = args[i+1] i++ } + case "--command": + if i+1 < len(args) { + command = args[i+1] + i++ + } case "-d", "--deliver": deliver = true + case "--delete-after": + deleteAfter = true case "--to": if i+1 < len(args) { to = args[i+1] @@ -166,13 +186,20 @@ func cronAddCmd(storePath string) { return } - if everySec == nil && cronExpr == "" { - fmt.Println("Error: Either --every or --cron must be specified") + if atSec == nil && everySec == nil && cronExpr == "" { + fmt.Println("Error: One of --at, --every, or --cron must be specified") return } var schedule cron.CronSchedule - if everySec != nil { + if atSec != nil { + atMS := time.Now().UnixMilli() + *atSec*1000 + schedule = cron.CronSchedule{ + Kind: "at", + AtMS: &atMS, + } + deleteAfter = true // at jobs always delete after run + } else if everySec != nil { everyMS := *everySec * 1000 schedule = cron.CronSchedule{ Kind: "every", @@ -185,6 +212,11 @@ func cronAddCmd(storePath string) { } } + // If command is set, deliver should be false + if command != "" { + deliver = false + } + cs := cron.NewCronService(storePath, nil) job, err := cs.AddJob(name, schedule, message, deliver, channel, to) if err != nil { @@ -192,6 +224,17 @@ func cronAddCmd(storePath string) { return } + // Set command and deleteAfterRun if needed + if command != "" { + job.Payload.Command = command + } + if deleteAfter { + job.DeleteAfterRun = true + } + if command != "" || deleteAfter { + cs.UpdateJob(job) + } + fmt.Printf("✓ Added job '%s' (%s)\n", job.Name, job.ID) } diff --git a/pkg/tools/cron.go b/pkg/tools/cron.go index 562fffc84..8d7c508a4 100644 --- a/pkg/tools/cron.go +++ b/pkg/tools/cron.go @@ -51,7 +51,7 @@ func (t *CronTool) Name() string { // Description returns the tool description func (t *CronTool) Description() string { - return "Schedule reminders, tasks, or system commands. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules. Use 'command' to execute shell commands directly." + return "Schedule reminders, tasks, or system commands. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Do NOT use system crontab or 'crontab -e'; always use this built-in cron tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules. Use 'command' to execute shell commands directly." } // Parameters returns the tool parameters schema @@ -299,6 +299,11 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string { ChatID: chatID, Content: output, }) + + // Reload store in case command modified cron/jobs.json directly + // (e.g., prayer auto_schedule writes AT jobs to the store file) + t.cronService.Load() + return "ok" } diff --git a/workspace/skills/prayer-times/SKILL.md b/workspace/skills/prayer-times/SKILL.md index 3227d09a0..b9a48c606 100644 --- a/workspace/skills/prayer-times/SKILL.md +++ b/workspace/skills/prayer-times/SKILL.md @@ -24,10 +24,10 @@ chmod +x skills/prayer-times/scripts/prayer_notify.sh | `today` | Tampilkan jadwal hari ini | | `schedule [prayers...]` | Output `nama\|HH:MM\|detik` untuk sholat yang belum lewat | | `notify