@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 +21 -0
- package/README.md +266 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +15042 -0
- package/package.json +60 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|