@staticduo/opencode-scheduler 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Different AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # opencode-scheduler
2
+
3
+ Run AI agents on a schedule. Set up recurring tasks that execute autonomously—even when you're away.
4
+
5
+ ```
6
+ Schedule a daily job at 9am to search Facebook Marketplace for posters under $100 and send the top 5 deals to my Telegram
7
+ ```
8
+
9
+ This is an [OpenCode](https://opencode.ai) plugin that uses your OS's native scheduler (launchd on macOS, systemd on Linux, Task Scheduler on Windows), with cron fallback where native backends are unavailable.
10
+
11
+ As of `v1.2.0`, jobs are scoped by `workdir` (so different projects don't collide), and scheduled runs are supervised (no overlap + optional timeout).
12
+
13
+ ## Fork changes (jibanez-staticduo, published as `@staticduo/opencode-scheduler`)
14
+
15
+ This fork contains three Linux/systemd fixes over upstream `v1.3.0`:
16
+
17
+ 1. **`cronToSystemdCalendars` emitted invalid `OnCalendar` values.** The
18
+ weekday component was always prefixed, even when the cron day-of-week field
19
+ is a wildcard, producing expressions like `* *-09-06 10:00:00`. systemd
20
+ does not accept `*` as a day-of-week, so every timer generated from such
21
+ schedules failed to parse. The fork omits the weekday prefix for wildcards
22
+ (e.g. `*-*-* 09:00:00`, `*-09-06 10:00:00`) and keeps generating both
23
+ valid variants when day-of-month and day-of-week are both fixed.
24
+ 2. **Failed systemd installs orphaned unit files.** `installSystemdJob` wrote
25
+ the `.service`/`.timer` units and then ran
26
+ `daemon-reload`/`enable`/`start`; if any of those systemctl calls failed,
27
+ the tool-level error handler removed the job JSON but left the unit files
28
+ behind (units without jobs). The fork rolls back freshly written units
29
+ before propagating the error, so neither units nor job JSON are orphaned.
30
+ 3. **`systemctl --user` calls inherited an incomplete environment.** When the
31
+ parent `opencode` process is started without `XDG_RUNTIME_DIR` (and
32
+ `DBUS_SESSION_BUS_ADDRESS`), every systemctl call failed with
33
+ `Failed to connect to bus` even though the user manager was running. All
34
+ systemctl invocations now go through a helper that derives
35
+ `XDG_RUNTIME_DIR=/run/user/<uid>` (and the bus address) when they exist.
36
+ Unit files are additionally written with mode `0644`, because hosts with a
37
+ lax umask (e.g. UGREEN NAS) generated permissions systemd complains about.
38
+
39
+ Unit tests for these fixes live in `test/` (`bun test`).
40
+
41
+ ## Install
42
+
43
+ Add to your `opencode.json`:
44
+
45
+ ```json
46
+ {
47
+ "plugin": ["@staticduo/opencode-scheduler"]
48
+ }
49
+ ```
50
+
51
+ ## Examples
52
+
53
+ **Daily deal hunting:**
54
+ ```
55
+ Schedule a daily job at 9am to search for standing desks under $300
56
+ ```
57
+
58
+ **Weekly reports:**
59
+ ```
60
+ Schedule a job every Monday at 8am to summarize my GitHub notifications
61
+ ```
62
+
63
+ **Recurring reminders:**
64
+ ```
65
+ Schedule a job every 6 hours to check if my website is up and alert me on Slack if it's down
66
+ ```
67
+
68
+ ## Commands
69
+
70
+ | Command | Example |
71
+ |---------|---------|
72
+ | Schedule a job | `Schedule a daily job at 9am to...` |
73
+ | List jobs | `Show my scheduled jobs` |
74
+ | Get version | `Show scheduler version` |
75
+ | Install skill template | `Install the scheduled job best practices skill` |
76
+ | Get job | `Show details for standing-desk` |
77
+ | Update job | `Update standing-desk to run at 10am` |
78
+ | Run immediately | `Run the standing-desk job now` |
79
+ | View logs | `Show logs for standing-desk` |
80
+ | Delete | `Delete the standing-desk job` |
81
+ | Global cleanup (dry run) | `Run scheduler global cleanup` |
82
+
83
+ ## How It Works
84
+
85
+ 1. You describe what you want scheduled in natural language
86
+ 2. The plugin writes a job file (scoped by `workdir`) and installs a timer in your OS scheduler
87
+ 3. At the scheduled time, the OS scheduler calls a small supervisor script
88
+ 4. The supervisor runs the job, appends logs, and updates job metadata
89
+
90
+ You can also trigger a job immediately via `run_job`—it runs fire-and-forget and appends to the same log file.
91
+
92
+ Jobs run from the working directory where you created them, picking up your `opencode.json` and MCP configurations.
93
+
94
+ ### Reliability Guarantees (Scheduled Runs)
95
+
96
+ - **No overlap**: if the previous run is still active, the next scheduled tick is skipped.
97
+ - **Non-interactive by default**: scheduled runs force `OPENCODE_PERMISSION` to deny "question" prompts, so jobs don't hang waiting for approvals.
98
+ - **Optional timeout**: set `timeoutSeconds` to hard-stop long runs (SIGTERM, then SIGKILL).
99
+
100
+ ### Platform Support
101
+
102
+ | Platform | Scheduler backend | Notes |
103
+ |------|------|------|
104
+ | macOS | `launchd` | Full support (supervised scheduled runs) |
105
+ | Linux (systemd available) | `systemd --user` | Full support (supervised scheduled runs) |
106
+ | Linux / POSIX (no systemd) | `cron` (`crontab`) | Fallback backend (no missed-run catch-up) |
107
+ | Windows | `schtasks` (Task Scheduler) | Supported with cron subset mapping (see limits below) |
108
+
109
+ Windows Task Scheduler limits:
110
+
111
+ - Cron expressions that use unsupported combinations (for example, month + weekday constraints, or month-only without explicit day-of-month) return a clear error with guidance.
112
+ - Complex cron schedules may be expanded into multiple Windows tasks under `\\OpenCode\\opencode-job-...`.
113
+ - Windows scheduled runs currently do **not** use the supervisor pipeline used on macOS/Linux, so no-overlap and timeout enforcement are not guaranteed by the OS integration itself.
114
+
115
+ ---
116
+
117
+ ## Reference
118
+
119
+ ### Cron Syntax
120
+
121
+ Jobs use standard 5-field cron expressions:
122
+
123
+ ```
124
+ ┌───────────── minute (0-59)
125
+ │ ┌───────────── hour (0-23)
126
+ │ │ ┌───────────── day of month (1-31)
127
+ │ │ │ ┌───────────── month (1-12)
128
+ │ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
129
+ │ │ │ │ │
130
+ * * * * *
131
+ ```
132
+
133
+ | Expression | Meaning |
134
+ |------------|---------|
135
+ | `0 9 * * *` | Daily at 9:00 AM |
136
+ | `0 */6 * * *` | Every 6 hours |
137
+ | `30 8 * * 1` | Mondays at 8:30 AM |
138
+ | `0 9,17 * * *` | At 9 AM and 5 PM daily |
139
+
140
+ ### Tools
141
+
142
+ | Tool | Description |
143
+ |------|-------------|
144
+ | `schedule_job` | Create a new scheduled job |
145
+ | `list_jobs` | List all scheduled jobs |
146
+ | `get_version` | Show scheduler and opencode versions |
147
+ | `get_skill` | Get built-in skill templates (best practices) |
148
+ | `install_skill` | Install a built-in skill into your repo |
149
+ | `get_job` | Fetch job details and metadata |
150
+ | `update_job` | Update an existing job |
151
+ | `delete_job` | Remove a scheduled job |
152
+ | `cleanup_global` | Remove scheduler artifacts across all scopes (dry-run by default) |
153
+ | `run_job` | Execute a job immediately (fire-and-forget) |
154
+ | `job_logs` | View the latest logs from a job |
155
+
156
+ `schedule_job` and `update_job` accept an optional `timeoutSeconds` (integer seconds). Use `0` (or omit) to disable.
157
+
158
+ Tools accept an optional `format: "json"` argument to return structured output with `success`, `output`, `shouldContinue`, and `data`.
159
+
160
+ ### Global Cleanup
161
+
162
+ Use `cleanup_global` to clean scheduler artifacts across all scopes. It always starts in dry-run mode unless you pass `confirm: true`.
163
+
164
+ - Dry run (safe default):
165
+
166
+ ```json
167
+ { "confirm": false }
168
+ ```
169
+
170
+ - Execute global cleanup of job definitions + lock files + scheduler units:
171
+
172
+ ```json
173
+ { "confirm": true }
174
+ ```
175
+
176
+ - Also delete logs and run history:
177
+
178
+ ```json
179
+ { "confirm": true, "includeHistory": true }
180
+ ```
181
+
182
+ The tool reports exactly how many artifacts were removed, grouped by location (jobs, locks, logs, runs, launchd/systemd units).
183
+
184
+ ### Storage
185
+
186
+ | What | Where |
187
+ |------|-------|
188
+ | Job configs (scoped) | `~/.config/opencode/scheduler/scopes/<scopeId>/jobs/*.json` |
189
+ | Run records (scoped) | `~/.config/opencode/scheduler/scopes/<scopeId>/runs/*.jsonl` |
190
+ | Locks (scoped) | `~/.config/opencode/scheduler/scopes/<scopeId>/locks/*.json` |
191
+ | Logs (scoped) | `~/.config/opencode/logs/scheduler/<scopeId>/*.log` |
192
+ | Supervisor script | `~/.config/opencode/scheduler/supervisor.pl` |
193
+ | launchd plists (Mac) | `~/Library/LaunchAgents/com.opencode.job.<scopeId>.*.plist` |
194
+ | systemd units (Linux) | `~/.config/systemd/user/opencode-job-<scopeId>-*.{service,timer}` |
195
+ | Task Scheduler entries (Windows) | `\\OpenCode\\opencode-job-<scopeId>-*` |
196
+
197
+ Legacy note: older versions stored jobs in `~/.config/opencode/jobs/*.json` and used unscoped unit names. `delete_job` removes both scoped and legacy artifacts.
198
+
199
+ ### Working Directory
200
+
201
+ Jobs run from a specific directory to pick up MCP configs:
202
+
203
+ ```
204
+ Schedule a daily job at 9am from /path/to/project to run my-task
205
+ ```
206
+
207
+ By default, jobs use the directory where you created them.
208
+
209
+ ### Scopes
210
+
211
+ Scopes are derived from the job's `workdir` (normalized absolute path). This isolates job storage, logs, and OS scheduler unit names per project.
212
+
213
+ - `list_jobs` defaults to the **current scope** (your current working directory).
214
+ - Use `allScopes: true` to list jobs across all scopes.
215
+ - Use `includeLegacy: true` to include pre-`v1.2.0` jobs stored in `~/.config/opencode/jobs`.
216
+
217
+ ### Attach URL (optional)
218
+
219
+ If you have an OpenCode backend running via `opencode serve` or `opencode web`, you can set `attachUrl` on a job so runs use that backend:
220
+
221
+ ```
222
+ Update the standing-desk job to use attachUrl http://localhost:4096
223
+ ```
224
+
225
+ ## Project Philosophy
226
+
227
+ - This plugin is intentionally a thin wrapper: it schedules `opencode run` via launchd/systemd/schtasks, with cron fallback when native backends are unavailable.
228
+ - Logs are the source of truth for scheduled runs: `~/.config/opencode/logs/*.log`.
229
+ - Resiliency/reporting roadmap (not implemented): `PRD-resilient-execution.md`.
230
+
231
+ ### Built-in Skill Templates
232
+
233
+ To install the built-in skill into your project (no copy/paste), open OpenCode in your repo and run:
234
+
235
+ ```
236
+ Install the scheduled job best practices skill
237
+ ```
238
+
239
+ This calls the plugin’s `install_skill` tool and writes `.opencode/skill/scheduled-job-best-practices/SKILL.md`.
240
+
241
+ (If you prefer, you can also say: `Get skill from opencode-scheduler and add it to my skills`.)
242
+
243
+ Then add `@scheduled-job-best-practices` at the top of scheduled job prompts.
244
+
245
+ (Manual option: use `get_skill` to fetch `scheduled-job-best-practices` and copy it into `.opencode/skill/scheduled-job-best-practices/SKILL.md`.)
246
+
247
+ ## Troubleshooting
248
+
249
+ **Jobs not running?**
250
+
251
+ 1. Check if installed:
252
+ - Mac: `launchctl list | grep opencode`
253
+ - Linux: `systemctl --user list-timers | grep opencode`
254
+ - Windows: `schtasks /Query /TN "\\OpenCode\\opencode-job-*"`
255
+
256
+ 2. Check logs: `Show logs for my-job`
257
+
258
+ 3. Verify the working directory has the right `opencode.json` with MCP configs
259
+
260
+ **MCP tools not available?**
261
+
262
+ Make sure the job's working directory contains an `opencode.json` with your MCP server configurations.
263
+
264
+ ## License
265
+
266
+ MIT
@@ -0,0 +1,83 @@
1
+ /**
2
+ * OpenCode Scheduler Plugin
3
+ *
4
+ * Schedule recurring jobs using launchd (Mac), systemd (Linux), schtasks (Windows), or cron fallback.
5
+ * Jobs are stored under ~/.config/opencode/scheduler/ (scoped by workdir).
6
+ *
7
+ * Features:
8
+ * - Survives reboots
9
+ * - Catches up on missed runs (if computer was asleep)
10
+ * - Cross-platform (Mac + Linux + Windows)
11
+ * - Working directory support for MCP configs
12
+ * - Environment variable injection (PATH for node/npx)
13
+ */
14
+ import type { Plugin } from "@opencode-ai/plugin";
15
+ import { execSync } from "child_process";
16
+ declare function jobFilePath(scopeId: string, slug: string): string;
17
+ type OpencodeRunFormat = "default" | "json";
18
+ interface JobRunSpec {
19
+ prompt?: string;
20
+ command?: string;
21
+ arguments?: string;
22
+ files?: string[];
23
+ agent?: string;
24
+ model?: string;
25
+ variant?: string;
26
+ title?: string;
27
+ share?: boolean;
28
+ continue?: boolean;
29
+ session?: string;
30
+ runFormat?: OpencodeRunFormat;
31
+ attachUrl?: string;
32
+ port?: number;
33
+ }
34
+ type JobInvocation = {
35
+ command: string;
36
+ args: string[];
37
+ };
38
+ interface Job {
39
+ scopeId?: string;
40
+ slug: string;
41
+ name: string;
42
+ schedule: string;
43
+ prompt?: string;
44
+ attachUrl?: string;
45
+ run?: JobRunSpec;
46
+ invocation?: JobInvocation;
47
+ timeoutSeconds?: number;
48
+ source?: string;
49
+ workdir?: string;
50
+ createdAt: string;
51
+ updatedAt?: string;
52
+ lastRunAt?: string;
53
+ lastRunExitCode?: number;
54
+ lastRunError?: string;
55
+ lastRunSource?: "manual" | "scheduled";
56
+ lastRunStatus?: "running" | "success" | "failed";
57
+ }
58
+ declare function cronToSystemdCalendars(cron: string): string[];
59
+ declare function withSystemdRuntimeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
60
+ declare function systemdRunEnv(): NodeJS.ProcessEnv;
61
+ type SystemdCommandRunner = (command: string, options?: Parameters<typeof execSync>[1]) => ReturnType<typeof execSync>;
62
+ declare function createSystemdTimer(job: Job): string;
63
+ declare function installSystemdJob(job: Job): void;
64
+ declare function uninstallSystemdJob(job: Job): void;
65
+ declare function saveJob(job: Job): void;
66
+ declare function deleteJobFile(job: Job): void;
67
+ export declare const SchedulerPlugin: Plugin;
68
+ export default SchedulerPlugin;
69
+ export type { SystemdCommandRunner };
70
+ export declare const __test__: {
71
+ cronToSystemdCalendars: typeof cronToSystemdCalendars;
72
+ createSystemdTimer: typeof createSystemdTimer;
73
+ withSystemdRuntimeEnv: typeof withSystemdRuntimeEnv;
74
+ systemdRunEnv: typeof systemdRunEnv;
75
+ installSystemdJob: typeof installSystemdJob;
76
+ uninstallSystemdJob: typeof uninstallSystemdJob;
77
+ saveJob: typeof saveJob;
78
+ deleteJobFile: typeof deleteJobFile;
79
+ jobFilePath: typeof jobFilePath;
80
+ SYSTEMD_USER_DIR: string;
81
+ SCOPES_DIR: string;
82
+ setSystemdCommandRunner(runner: SystemdCommandRunner | null): void;
83
+ };