cron-claude 1.0.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) 2026 tygi
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,260 @@
1
+ # cron-claude
2
+
3
+ Schedule any Claude prompt to run automatically on a cron. Add a job, set a schedule, forget about it.
4
+
5
+ [![CI](https://github.com/tarsbotty/cron-claude/actions/workflows/ci.yml/badge.svg)](https://github.com/tarsbotty/cron-claude/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ ---
9
+
10
+ ## What it does
11
+
12
+ `cron-claude` is a lightweight daemon that runs `claude` CLI commands on a schedule. Define a prompt, set a cron expression, and the daemon fires it automatically — logging output to `~/.cron-claude/logs/`. It runs as a macOS LaunchAgent so it survives reboots, and it reloads jobs live when you edit the config. No server to babysit, no web UI, just prompts on a schedule.
13
+
14
+ You can manage jobs from the CLI, from Claude Desktop or Claude Code via the bundled MCP server, or from Claude chat via the OpenClaw plugin.
15
+
16
+ ---
17
+
18
+ ## Why not just use OpenClaw?
19
+
20
+ OpenClaw's scheduled agentic tasks are genuinely powerful — prompts that run on a timer, check things, take actions, and report back. But OpenClaw is a full personal AI platform with broad system access: files, email, calendar, browser, messaging. That's the point of it, but it's also a wide attack surface.
21
+
22
+ `cron-claude` is just that one feature. Each job is a single `claude` invocation with a prompt you wrote — the blast radius is scoped to what that one call can do, on your terms, with no credentials stored in a platform. If you want scheduled agentic tasks without the overhead of a full AI stack, this is the trade-off.
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install -g cron-claude
30
+ cron-claude install-service # sets up macOS LaunchAgent (auto-starts on login)
31
+ ```
32
+
33
+ **Requirements:** Node.js 22+, `claude` CLI installed and authenticated (`~/.local/bin/claude`).
34
+
35
+ ---
36
+
37
+ ## Quick Start
38
+
39
+ ```bash
40
+ # Add a job that runs daily at 9 AM
41
+ cron-claude add \
42
+ --id morning-brief \
43
+ --schedule "0 9 * * *" \
44
+ --prompt "Check my email inbox for anything urgent and summarize it."
45
+
46
+ # Confirm it's scheduled
47
+ cron-claude list
48
+ # ID Schedule Model Enabled Prompt
49
+ # --------------------------------------------------------------------------------
50
+ # morning-brief 0 9 * * * sonnet true Check my email inbox for anything urgent...
51
+
52
+ # Check overall status
53
+ cron-claude status
54
+ # Cron-Claude Status
55
+ # Total jobs: 1
56
+ # Enabled: 1
57
+ # Disabled: 0
58
+ ```
59
+
60
+ ---
61
+
62
+ ## CLI Reference
63
+
64
+ ### `add`
65
+
66
+ ```bash
67
+ cron-claude add --id <id> --schedule "<cron>" --prompt "<prompt>" [--model <model>] [--disabled]
68
+ ```
69
+
70
+ | Flag | Required | Default | Description |
71
+ |------|----------|---------|-------------|
72
+ | `--id` | ✅ | — | Unique job identifier |
73
+ | `--schedule` | ✅ | — | Cron expression (5-field) |
74
+ | `--prompt` | ✅ | — | Prompt sent to `claude` |
75
+ | `--model` | | `sonnet` | Claude model (`sonnet`, `opus`, `haiku`) |
76
+ | `--disabled` | | `false` | Create job in disabled state |
77
+
78
+ **Examples:**
79
+
80
+ ```bash
81
+ # Daily at 2 PM
82
+ cron-claude add --id daily-summary --schedule "0 14 * * *" --prompt "Summarize my day and post to Discord"
83
+
84
+ # Every Monday at 9 AM with Opus
85
+ cron-claude add --id weekly-review --schedule "0 9 * * 1" --prompt "Review open GitHub issues and write a priority list" --model opus
86
+
87
+ # Add but keep disabled until ready
88
+ cron-claude add --id nightly-report --schedule "0 23 * * *" --prompt "Generate nightly stats" --disabled
89
+ ```
90
+
91
+ ### `list`
92
+
93
+ ```bash
94
+ cron-claude list
95
+ ```
96
+
97
+ Prints a table of all configured jobs: ID, schedule, model, enabled state, and prompt preview.
98
+
99
+ ### `remove`
100
+
101
+ ```bash
102
+ cron-claude remove <id>
103
+ ```
104
+
105
+ Removes the job from `jobs.json`. The daemon reloads automatically.
106
+
107
+ ### `enable` / `disable`
108
+
109
+ ```bash
110
+ cron-claude enable <id>
111
+ cron-claude disable <id>
112
+ ```
113
+
114
+ Toggle a job on or off without removing it.
115
+
116
+ ### `status`
117
+
118
+ ```bash
119
+ cron-claude status
120
+ ```
121
+
122
+ Shows total, enabled, and disabled job counts.
123
+
124
+ ---
125
+
126
+ ## MCP Server (Claude Desktop / Claude Code)
127
+
128
+ `cron-claude` ships an [MCP](https://modelcontextprotocol.io) server so you can manage jobs directly from Claude Desktop, Claude Code, or any MCP client — no terminal needed.
129
+
130
+ ### Claude Desktop
131
+
132
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
133
+
134
+ ```json
135
+ {
136
+ "mcpServers": {
137
+ "cron-claude": {
138
+ "command": "cron-claude-mcp"
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ Restart Claude Desktop. You can then say *"add a cron job that runs every morning and checks the weather"* and it'll use the tools directly.
145
+
146
+ ### Claude Code
147
+
148
+ ```bash
149
+ claude mcp add cron-claude -- cron-claude-mcp
150
+ ```
151
+
152
+ Or add to `.mcp.json` manually:
153
+
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "cron-claude": {
158
+ "command": "cron-claude-mcp"
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Available MCP Tools
165
+
166
+ | Tool | Description | Parameters |
167
+ |------|-------------|------------|
168
+ | `add_cron` | Add a new scheduled job | `id`, `schedule`, `prompt`, `model?`, `disabled?` |
169
+ | `list_crons` | List all configured jobs | — |
170
+ | `remove_cron` | Remove a job by ID | `id` |
171
+ | `enable_cron` | Enable a disabled job | `id` |
172
+ | `disable_cron` | Disable a job without removing it | `id` |
173
+ | `get_status` | Daemon status + job counts | — |
174
+
175
+ ---
176
+
177
+ ## Claude Code Plugin
178
+
179
+ Install `cron-claude` from the Claude Code plugin marketplace:
180
+
181
+ ```bash
182
+ claude plugin marketplace add github:tarsbotty/cron-claude
183
+ claude plugin install cron-claude
184
+ ```
185
+
186
+ Claude Code will automatically connect to the `cron-claude` MCP server, exposing all management tools without leaving your editor.
187
+
188
+ ---
189
+
190
+ ## OpenClaw Skill
191
+
192
+ For OpenClaw users, the skill in `src/plugin/` exposes natural-language cron management via `SKILL.md`. Copy `src/plugin/` into your OpenClaw skills directory or reference it in your OpenClaw config.
193
+
194
+ ---
195
+
196
+ ## Config
197
+
198
+ Jobs are stored in `~/.cron-claude/jobs.json`. The daemon watches this file and reloads automatically on any change — no restart required.
199
+
200
+ **Example `jobs.json`:**
201
+
202
+ ```json
203
+ {
204
+ "jobs": [
205
+ {
206
+ "id": "morning-brief",
207
+ "schedule": "0 9 * * *",
208
+ "prompt": "Check my email inbox for anything urgent and summarize it.",
209
+ "model": "sonnet",
210
+ "enabled": true
211
+ },
212
+ {
213
+ "id": "weekly-review",
214
+ "schedule": "0 9 * * 1",
215
+ "prompt": "Review open GitHub issues and write a priority list.",
216
+ "model": "opus",
217
+ "enabled": true
218
+ }
219
+ ]
220
+ }
221
+ ```
222
+
223
+ ### Cron Syntax
224
+
225
+ Standard 5-field format: `minute hour day-of-month month day-of-week`
226
+
227
+ | Expression | Meaning |
228
+ |------------|---------|
229
+ | `0 9 * * *` | Daily at 9:00 AM |
230
+ | `0 14 * * *` | Daily at 2:00 PM |
231
+ | `*/30 * * * *` | Every 30 minutes |
232
+ | `0 9 * * 1` | Mondays at 9:00 AM |
233
+ | `0 0 1 * *` | First of each month at midnight |
234
+
235
+ ---
236
+
237
+ ## How it works
238
+
239
+ A macOS LaunchAgent keeps the `cron-claude` daemon alive across reboots. On startup, the daemon loads `~/.cron-claude/jobs.json`, validates each job with [Zod](https://zod.dev), and registers schedules using [node-cron](https://github.com/node-cron/node-cron). When a job fires, the daemon calls `execFile` on the `claude` CLI with `--print --model <model> <prompt>` and a 5-minute timeout. Output (or any error) is written to a timestamped log file in `~/.cron-claude/logs/`. The config file is watched via `fs.watch`; any edit triggers a debounced reload so you never need to restart the daemon manually.
240
+
241
+ ---
242
+
243
+ ## Development
244
+
245
+ ```bash
246
+ git clone git@github.com:tarsbotty/cron-claude.git
247
+ cd cron-claude
248
+ npm install
249
+ npm run build # compile TypeScript → dist/
250
+ npm test # run vitest with coverage (90% threshold enforced)
251
+ npm run dev # run daemon directly with tsx (no build step)
252
+ ```
253
+
254
+ **Test suite:** `tests/` covers the config layer, scheduler, daemon, CLI, and MCP server handlers. Coverage thresholds are enforced in `vitest.config.ts` (statements/functions/lines: 90%, branches: 85%).
255
+
256
+ ---
257
+
258
+ ## License
259
+
260
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ declare const program: Command;
4
+ export { program };
package/dist/cli.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { addJob, removeJob, listJobs, toggleJob, JobSchema } from './config.js';
4
+ const program = new Command();
5
+ program
6
+ .name('cron-claude')
7
+ .description('Manage scheduled Claude CLI jobs')
8
+ .version('1.0.0');
9
+ program
10
+ .command('list')
11
+ .description('List all scheduled jobs')
12
+ .action(() => {
13
+ const jobs = listJobs();
14
+ if (jobs.length === 0) {
15
+ console.log('No jobs configured.');
16
+ return;
17
+ }
18
+ console.log(`\n${'ID'.padEnd(20)} ${'Schedule'.padEnd(15)} ${'Model'.padEnd(10)} ${'Enabled'.padEnd(8)} Prompt`);
19
+ console.log('-'.repeat(80));
20
+ for (const job of jobs) {
21
+ console.log(`${job.id.padEnd(20)} ${job.schedule.padEnd(15)} ${job.model.padEnd(10)} ${String(job.enabled).padEnd(8)} ${job.prompt.slice(0, 40)}`);
22
+ }
23
+ console.log('');
24
+ });
25
+ program
26
+ .command('add')
27
+ .description('Add a new scheduled job')
28
+ .requiredOption('--id <id>', 'Unique job identifier')
29
+ .requiredOption('--schedule <cron>', 'Cron schedule expression')
30
+ .requiredOption('--prompt <prompt>', 'Prompt to send to Claude')
31
+ .option('--model <model>', 'Claude model to use', 'sonnet')
32
+ .option('--disabled', 'Create job in disabled state')
33
+ .action((opts) => {
34
+ try {
35
+ const job = JobSchema.parse({
36
+ id: opts.id,
37
+ schedule: opts.schedule,
38
+ prompt: opts.prompt,
39
+ model: opts.model,
40
+ enabled: !opts.disabled,
41
+ });
42
+ addJob(job);
43
+ console.log(`Job "${job.id}" added successfully.`);
44
+ }
45
+ catch (err) {
46
+ console.error(`Error: ${err.message}`);
47
+ process.exit(1);
48
+ }
49
+ });
50
+ program
51
+ .command('remove <id>')
52
+ .description('Remove a scheduled job')
53
+ .action((id) => {
54
+ try {
55
+ removeJob(id);
56
+ console.log(`Job "${id}" removed.`);
57
+ }
58
+ catch (err) {
59
+ console.error(`Error: ${err.message}`);
60
+ process.exit(1);
61
+ }
62
+ });
63
+ program
64
+ .command('enable <id>')
65
+ .description('Enable a job')
66
+ .action((id) => {
67
+ try {
68
+ toggleJob(id, true);
69
+ console.log(`Job "${id}" enabled.`);
70
+ }
71
+ catch (err) {
72
+ console.error(`Error: ${err.message}`);
73
+ process.exit(1);
74
+ }
75
+ });
76
+ program
77
+ .command('disable <id>')
78
+ .description('Disable a job')
79
+ .action((id) => {
80
+ try {
81
+ toggleJob(id, false);
82
+ console.log(`Job "${id}" disabled.`);
83
+ }
84
+ catch (err) {
85
+ console.error(`Error: ${err.message}`);
86
+ process.exit(1);
87
+ }
88
+ });
89
+ program
90
+ .command('status')
91
+ .description('Show daemon status')
92
+ .action(() => {
93
+ const jobs = listJobs();
94
+ const enabled = jobs.filter((j) => j.enabled).length;
95
+ const disabled = jobs.filter((j) => !j.enabled).length;
96
+ console.log(`\nCron-Claude Status`);
97
+ console.log(` Total jobs: ${jobs.length}`);
98
+ console.log(` Enabled: ${enabled}`);
99
+ console.log(` Disabled: ${disabled}\n`);
100
+ });
101
+ export { program };
102
+ // Run if executed directly
103
+ const isMain = process.argv[1]?.endsWith('cli.ts') || process.argv[1]?.endsWith('cli.js');
104
+ if (isMain) {
105
+ program.parse();
106
+ }
107
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ export declare const JobSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ schedule: z.ZodString;
5
+ prompt: z.ZodString;
6
+ model: z.ZodDefault<z.ZodString>;
7
+ enabled: z.ZodDefault<z.ZodBoolean>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ id: string;
10
+ schedule: string;
11
+ prompt: string;
12
+ model: string;
13
+ enabled: boolean;
14
+ }, {
15
+ id: string;
16
+ schedule: string;
17
+ prompt: string;
18
+ model?: string | undefined;
19
+ enabled?: boolean | undefined;
20
+ }>;
21
+ export declare const ConfigSchema: z.ZodObject<{
22
+ jobs: z.ZodArray<z.ZodObject<{
23
+ id: z.ZodString;
24
+ schedule: z.ZodString;
25
+ prompt: z.ZodString;
26
+ model: z.ZodDefault<z.ZodString>;
27
+ enabled: z.ZodDefault<z.ZodBoolean>;
28
+ }, "strip", z.ZodTypeAny, {
29
+ id: string;
30
+ schedule: string;
31
+ prompt: string;
32
+ model: string;
33
+ enabled: boolean;
34
+ }, {
35
+ id: string;
36
+ schedule: string;
37
+ prompt: string;
38
+ model?: string | undefined;
39
+ enabled?: boolean | undefined;
40
+ }>, "many">;
41
+ }, "strip", z.ZodTypeAny, {
42
+ jobs: {
43
+ id: string;
44
+ schedule: string;
45
+ prompt: string;
46
+ model: string;
47
+ enabled: boolean;
48
+ }[];
49
+ }, {
50
+ jobs: {
51
+ id: string;
52
+ schedule: string;
53
+ prompt: string;
54
+ model?: string | undefined;
55
+ enabled?: boolean | undefined;
56
+ }[];
57
+ }>;
58
+ export type Job = z.infer<typeof JobSchema>;
59
+ export type Config = z.infer<typeof ConfigSchema>;
60
+ export declare function getConfigDir(): string;
61
+ export declare function getConfigPath(): string;
62
+ export declare function getLogsDir(): string;
63
+ export declare function ensureConfigDir(): void;
64
+ export declare function loadConfig(configPath?: string): Config;
65
+ export declare function saveConfig(config: Config, configPath?: string): void;
66
+ export declare function addJob(job: Job, configPath?: string): Config;
67
+ export declare function removeJob(jobId: string, configPath?: string): Config;
68
+ export declare function listJobs(configPath?: string): Job[];
69
+ export declare function toggleJob(jobId: string, enabled: boolean, configPath?: string): Config;
package/dist/config.js ADDED
@@ -0,0 +1,96 @@
1
+ import { z } from 'zod';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ export const JobSchema = z.object({
5
+ id: z.string().min(1),
6
+ schedule: z.string().min(1),
7
+ prompt: z.string().min(1),
8
+ model: z.string().default('sonnet'),
9
+ enabled: z.boolean().default(true),
10
+ });
11
+ export const ConfigSchema = z.object({
12
+ jobs: z.array(JobSchema),
13
+ });
14
+ const CONFIG_DIR = path.join(process.env.HOME || '~', '.cron-claude');
15
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'jobs.json');
16
+ const LOGS_DIR = path.join(CONFIG_DIR, 'logs');
17
+ export function getConfigDir() {
18
+ return CONFIG_DIR;
19
+ }
20
+ export function getConfigPath() {
21
+ return CONFIG_FILE;
22
+ }
23
+ export function getLogsDir() {
24
+ return LOGS_DIR;
25
+ }
26
+ const EXAMPLE_CONFIG = {
27
+ jobs: [
28
+ {
29
+ id: 'daily-summary',
30
+ schedule: '0 14 * * *',
31
+ prompt: 'Summarize my day and send to Discord',
32
+ model: 'sonnet',
33
+ enabled: true,
34
+ },
35
+ ],
36
+ };
37
+ export function ensureConfigDir() {
38
+ if (!fs.existsSync(CONFIG_DIR)) {
39
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
40
+ }
41
+ if (!fs.existsSync(LOGS_DIR)) {
42
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
43
+ }
44
+ }
45
+ export function loadConfig(configPath) {
46
+ const filePath = configPath ?? CONFIG_FILE;
47
+ ensureConfigDir();
48
+ if (!fs.existsSync(filePath)) {
49
+ const config = EXAMPLE_CONFIG;
50
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
51
+ return config;
52
+ }
53
+ const raw = fs.readFileSync(filePath, 'utf-8');
54
+ const parsed = JSON.parse(raw);
55
+ return ConfigSchema.parse(parsed);
56
+ }
57
+ export function saveConfig(config, configPath) {
58
+ const filePath = configPath ?? CONFIG_FILE;
59
+ ensureConfigDir();
60
+ ConfigSchema.parse(config);
61
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
62
+ }
63
+ export function addJob(job, configPath) {
64
+ const config = loadConfig(configPath);
65
+ if (config.jobs.some((j) => j.id === job.id)) {
66
+ throw new Error(`Job with id "${job.id}" already exists`);
67
+ }
68
+ config.jobs.push(job);
69
+ saveConfig(config, configPath);
70
+ return config;
71
+ }
72
+ export function removeJob(jobId, configPath) {
73
+ const config = loadConfig(configPath);
74
+ const idx = config.jobs.findIndex((j) => j.id === jobId);
75
+ if (idx === -1) {
76
+ throw new Error(`Job with id "${jobId}" not found`);
77
+ }
78
+ config.jobs.splice(idx, 1);
79
+ saveConfig(config, configPath);
80
+ return config;
81
+ }
82
+ export function listJobs(configPath) {
83
+ const config = loadConfig(configPath);
84
+ return config.jobs;
85
+ }
86
+ export function toggleJob(jobId, enabled, configPath) {
87
+ const config = loadConfig(configPath);
88
+ const job = config.jobs.find((j) => j.id === jobId);
89
+ if (!job) {
90
+ throw new Error(`Job with id "${jobId}" not found`);
91
+ }
92
+ job.enabled = enabled;
93
+ saveConfig(config, configPath);
94
+ return config;
95
+ }
96
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { Scheduler } from './scheduler.js';
2
+ export declare class Daemon {
3
+ private scheduler;
4
+ private watcher;
5
+ private configPath;
6
+ constructor(configPath?: string);
7
+ start(): void;
8
+ stop(): void;
9
+ getScheduler(): Scheduler;
10
+ private loadAndSchedule;
11
+ private watchConfig;
12
+ }
13
+ export declare function main(): void;
package/dist/daemon.js ADDED
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import { loadConfig, getConfigPath, ensureConfigDir } from './config.js';
3
+ import { Scheduler } from './scheduler.js';
4
+ export class Daemon {
5
+ scheduler;
6
+ watcher = null;
7
+ configPath;
8
+ constructor(configPath) {
9
+ this.configPath = configPath ?? getConfigPath();
10
+ this.scheduler = new Scheduler({
11
+ onJobStart: (job) => {
12
+ console.log(`[cron-claude] Running job: ${job.id}`);
13
+ },
14
+ onJobComplete: (job, output) => {
15
+ console.log(`[cron-claude] Job ${job.id} completed. Output: ${output.slice(0, 200)}`);
16
+ },
17
+ onJobError: (job, error) => {
18
+ console.error(`[cron-claude] Job ${job.id} failed: ${error.message}`);
19
+ },
20
+ });
21
+ }
22
+ start() {
23
+ ensureConfigDir();
24
+ console.log('[cron-claude] Daemon starting...');
25
+ this.loadAndSchedule();
26
+ this.watchConfig();
27
+ console.log('[cron-claude] Daemon running. Watching for config changes.');
28
+ }
29
+ stop() {
30
+ console.log('[cron-claude] Daemon stopping...');
31
+ this.scheduler.unscheduleAll();
32
+ if (this.watcher) {
33
+ this.watcher.close();
34
+ this.watcher = null;
35
+ }
36
+ console.log('[cron-claude] Daemon stopped.');
37
+ }
38
+ getScheduler() {
39
+ return this.scheduler;
40
+ }
41
+ loadAndSchedule() {
42
+ try {
43
+ const config = loadConfig(this.configPath);
44
+ this.scheduler.unscheduleAll();
45
+ for (const job of config.jobs) {
46
+ try {
47
+ this.scheduler.schedule(job);
48
+ console.log(`[cron-claude] Scheduled: ${job.id} (${job.schedule})`);
49
+ }
50
+ catch (err) {
51
+ console.error(`[cron-claude] Failed to schedule ${job.id}: ${err.message}`);
52
+ }
53
+ }
54
+ console.log(`[cron-claude] ${this.scheduler.getScheduledIds().length} jobs scheduled.`);
55
+ }
56
+ catch (err) {
57
+ console.error(`[cron-claude] Failed to load config: ${err.message}`);
58
+ }
59
+ }
60
+ watchConfig() {
61
+ let debounce = null;
62
+ try {
63
+ this.watcher = fs.watch(this.configPath, () => {
64
+ if (debounce)
65
+ clearTimeout(debounce);
66
+ debounce = setTimeout(() => {
67
+ console.log('[cron-claude] Config changed, reloading...');
68
+ this.loadAndSchedule();
69
+ }, 500);
70
+ });
71
+ }
72
+ catch {
73
+ console.warn('[cron-claude] Could not watch config file. Changes require restart.');
74
+ }
75
+ }
76
+ }
77
+ /* c8 ignore start */
78
+ export function main() {
79
+ const daemon = new Daemon();
80
+ daemon.start();
81
+ process.on('SIGINT', () => {
82
+ daemon.stop();
83
+ process.exit(0);
84
+ });
85
+ process.on('SIGTERM', () => {
86
+ daemon.stop();
87
+ process.exit(0);
88
+ });
89
+ }
90
+ const isMain = process.argv[1]?.endsWith('daemon.ts') || process.argv[1]?.endsWith('daemon.js');
91
+ if (isMain) {
92
+ main();
93
+ }
94
+ /* c8 ignore stop */
95
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.js","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP (Model Context Protocol) server for cron-claude.
4
+ * Exposes cron job management as structured tool calls for Claude Desktop,
5
+ * Claude Code, and any MCP-compatible client.
6
+ */
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ export type ToolResult = {
9
+ content: Array<{
10
+ type: 'text';
11
+ text: string;
12
+ }>;
13
+ isError?: boolean;
14
+ };
15
+ export declare function handleAddCron(args: {
16
+ id: string;
17
+ schedule: string;
18
+ prompt: string;
19
+ model?: string;
20
+ disabled?: boolean;
21
+ }): Promise<ToolResult>;
22
+ export declare function handleListCrons(): Promise<ToolResult>;
23
+ export declare function handleRemoveCron(args: {
24
+ id: string;
25
+ }): Promise<ToolResult>;
26
+ export declare function handleEnableCron(args: {
27
+ id: string;
28
+ }): Promise<ToolResult>;
29
+ export declare function handleDisableCron(args: {
30
+ id: string;
31
+ }): Promise<ToolResult>;
32
+ export declare function handleGetStatus(): Promise<ToolResult>;
33
+ export declare const server: McpServer;
34
+ export declare function main(): Promise<void>;
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP (Model Context Protocol) server for cron-claude.
4
+ * Exposes cron job management as structured tool calls for Claude Desktop,
5
+ * Claude Code, and any MCP-compatible client.
6
+ */
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { z } from 'zod';
10
+ import { addJob, removeJob, listJobs, toggleJob, JobSchema } from './config.js';
11
+ import { exec } from 'node:child_process';
12
+ import { promisify } from 'node:util';
13
+ const execAsync = promisify(exec);
14
+ // ─── Named handler functions (exported for testability + V8 coverage) ─────────
15
+ export async function handleAddCron(args) {
16
+ try {
17
+ const job = JobSchema.parse({
18
+ id: args.id,
19
+ schedule: args.schedule,
20
+ prompt: args.prompt,
21
+ model: args.model ?? 'sonnet',
22
+ enabled: !(args.disabled ?? false),
23
+ });
24
+ addJob(job);
25
+ return {
26
+ content: [
27
+ {
28
+ type: 'text',
29
+ text: `Job "${job.id}" added successfully.\nSchedule: ${job.schedule}\nModel: ${job.model}\nEnabled: ${job.enabled}`,
30
+ },
31
+ ],
32
+ };
33
+ }
34
+ catch (err) {
35
+ return {
36
+ content: [{ type: 'text', text: `Error adding job: ${err.message}` }],
37
+ isError: true,
38
+ };
39
+ }
40
+ }
41
+ export async function handleListCrons() {
42
+ try {
43
+ const jobs = listJobs();
44
+ if (jobs.length === 0) {
45
+ return {
46
+ content: [{ type: 'text', text: 'No cron jobs configured.' }],
47
+ };
48
+ }
49
+ const lines = jobs.map((job) => `• ${job.id}\n Schedule: ${job.schedule}\n Model: ${job.model}\n Enabled: ${job.enabled}\n Prompt: ${job.prompt}`);
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: `${jobs.length} job(s) configured:\n\n${lines.join('\n\n')}`,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+ catch (err) {
60
+ return {
61
+ content: [{ type: 'text', text: `Error listing jobs: ${err.message}` }],
62
+ isError: true,
63
+ };
64
+ }
65
+ }
66
+ export async function handleRemoveCron(args) {
67
+ try {
68
+ removeJob(args.id);
69
+ return {
70
+ content: [{ type: 'text', text: `Job "${args.id}" removed successfully.` }],
71
+ };
72
+ }
73
+ catch (err) {
74
+ return {
75
+ content: [{ type: 'text', text: `Error removing job: ${err.message}` }],
76
+ isError: true,
77
+ };
78
+ }
79
+ }
80
+ export async function handleEnableCron(args) {
81
+ try {
82
+ toggleJob(args.id, true);
83
+ return {
84
+ content: [{ type: 'text', text: `Job "${args.id}" enabled successfully.` }],
85
+ };
86
+ }
87
+ catch (err) {
88
+ return {
89
+ content: [{ type: 'text', text: `Error enabling job: ${err.message}` }],
90
+ isError: true,
91
+ };
92
+ }
93
+ }
94
+ export async function handleDisableCron(args) {
95
+ try {
96
+ toggleJob(args.id, false);
97
+ return {
98
+ content: [{ type: 'text', text: `Job "${args.id}" disabled successfully.` }],
99
+ };
100
+ }
101
+ catch (err) {
102
+ return {
103
+ content: [{ type: 'text', text: `Error disabling job: ${err.message}` }],
104
+ isError: true,
105
+ };
106
+ }
107
+ }
108
+ export async function handleGetStatus() {
109
+ try {
110
+ const jobs = listJobs();
111
+ const enabled = jobs.filter((j) => j.enabled).length;
112
+ const disabled = jobs.filter((j) => !j.enabled).length;
113
+ let daemonStatus = 'unknown';
114
+ try {
115
+ const { stdout } = await execAsync('pgrep -f "cron-claude"');
116
+ daemonStatus = stdout.trim() ? 'running' : 'stopped';
117
+ }
118
+ catch {
119
+ daemonStatus = 'stopped';
120
+ }
121
+ const lines = [
122
+ `Daemon status: ${daemonStatus}`,
123
+ `Total jobs: ${jobs.length}`,
124
+ `Enabled: ${enabled}`,
125
+ `Disabled: ${disabled}`,
126
+ ];
127
+ return {
128
+ content: [{ type: 'text', text: lines.join('\n') }],
129
+ };
130
+ }
131
+ catch (err) {
132
+ return {
133
+ content: [{ type: 'text', text: `Error getting status: ${err.message}` }],
134
+ isError: true,
135
+ };
136
+ }
137
+ }
138
+ // ─── Server setup ─────────────────────────────────────────────────────────────
139
+ export const server = new McpServer({
140
+ name: 'cron-claude',
141
+ version: '1.0.0',
142
+ });
143
+ server.tool('add_cron', 'Add a new scheduled cron job that runs a Claude prompt on a schedule', {
144
+ id: z.string().min(1).describe('Unique identifier for the job'),
145
+ schedule: z
146
+ .string()
147
+ .min(1)
148
+ .describe('Cron schedule expression (e.g. "0 14 * * *" for daily at 2pm)'),
149
+ prompt: z.string().min(1).describe('Prompt to send to Claude when the job runs'),
150
+ model: z.string().optional().describe('Claude model to use (default: sonnet)'),
151
+ disabled: z
152
+ .boolean()
153
+ .optional()
154
+ .describe('Create the job in a disabled state (default: false)'),
155
+ }, handleAddCron);
156
+ server.tool('list_crons', 'List all configured cron jobs', {}, async () => handleListCrons());
157
+ server.tool('remove_cron', 'Remove a scheduled cron job by ID', {
158
+ id: z.string().min(1).describe('ID of the job to remove'),
159
+ }, handleRemoveCron);
160
+ server.tool('enable_cron', 'Enable a previously disabled cron job', {
161
+ id: z.string().min(1).describe('ID of the job to enable'),
162
+ }, handleEnableCron);
163
+ server.tool('disable_cron', 'Disable a cron job without removing it', {
164
+ id: z.string().min(1).describe('ID of the job to disable'),
165
+ }, handleDisableCron);
166
+ server.tool('get_status', 'Get the current status of the cron-claude daemon and job counts', {}, async () => handleGetStatus());
167
+ // ─── Main entry point ─────────────────────────────────────────────────────────
168
+ export async function main() {
169
+ const transport = new StdioServerTransport();
170
+ await server.connect(transport);
171
+ }
172
+ /* c8 ignore start */
173
+ const isMain = process.argv[1]?.endsWith('mcp-server.ts') || process.argv[1]?.endsWith('mcp-server.js');
174
+ if (isMain) {
175
+ main().catch((err) => {
176
+ console.error('MCP server error:', err);
177
+ process.exit(1);
178
+ });
179
+ }
180
+ /* c8 ignore stop */
181
+ //# sourceMappingURL=mcp-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { Job } from './config.js';
2
+ export interface SchedulerOptions {
3
+ claudePath?: string;
4
+ onJobStart?: (job: Job) => void;
5
+ onJobComplete?: (job: Job, output: string) => void;
6
+ onJobError?: (job: Job, error: Error) => void;
7
+ }
8
+ export declare class Scheduler {
9
+ private tasks;
10
+ private options;
11
+ constructor(options?: SchedulerOptions);
12
+ getClaudePath(): string;
13
+ schedule(job: Job): void;
14
+ unschedule(jobId: string): void;
15
+ unscheduleAll(): void;
16
+ getScheduledIds(): string[];
17
+ executeJob(job: Job): Promise<string>;
18
+ private logJobOutput;
19
+ }
@@ -0,0 +1,87 @@
1
+ import cron from 'node-cron';
2
+ import { execFile } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { getLogsDir } from './config.js';
6
+ const CLAUDE_PATH = path.join(process.env.HOME || '~', '.local', 'bin', 'claude');
7
+ export class Scheduler {
8
+ tasks = new Map();
9
+ options;
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ }
13
+ getClaudePath() {
14
+ return this.options.claudePath ?? CLAUDE_PATH;
15
+ }
16
+ schedule(job) {
17
+ if (!job.enabled)
18
+ return;
19
+ if (!cron.validate(job.schedule)) {
20
+ throw new Error(`Invalid cron schedule for job "${job.id}": ${job.schedule}`);
21
+ }
22
+ // Stop existing task if re-scheduling
23
+ this.unschedule(job.id);
24
+ const task = cron.schedule(job.schedule, () => {
25
+ this.executeJob(job);
26
+ });
27
+ this.tasks.set(job.id, task);
28
+ }
29
+ unschedule(jobId) {
30
+ const existing = this.tasks.get(jobId);
31
+ if (existing) {
32
+ existing.stop();
33
+ this.tasks.delete(jobId);
34
+ }
35
+ }
36
+ unscheduleAll() {
37
+ for (const [id] of this.tasks) {
38
+ this.unschedule(id);
39
+ }
40
+ }
41
+ getScheduledIds() {
42
+ return Array.from(this.tasks.keys());
43
+ }
44
+ executeJob(job) {
45
+ return new Promise((resolve, reject) => {
46
+ this.options.onJobStart?.(job);
47
+ const args = ['--print', '--model', job.model, job.prompt];
48
+ const claudePath = this.getClaudePath();
49
+ execFile(claudePath, args, { timeout: 300000 }, (error, stdout, stderr) => {
50
+ const output = stdout || stderr || '';
51
+ this.logJobOutput(job, output, error ?? undefined);
52
+ if (error) {
53
+ this.options.onJobError?.(job, error);
54
+ reject(error);
55
+ }
56
+ else {
57
+ this.options.onJobComplete?.(job, output);
58
+ resolve(output);
59
+ }
60
+ });
61
+ });
62
+ }
63
+ logJobOutput(job, output, error) {
64
+ const logsDir = getLogsDir();
65
+ try {
66
+ if (!fs.existsSync(logsDir)) {
67
+ fs.mkdirSync(logsDir, { recursive: true });
68
+ }
69
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
70
+ const logFile = path.join(logsDir, `${job.id}_${timestamp}.log`);
71
+ const content = [
72
+ `Job: ${job.id}`,
73
+ `Time: ${new Date().toISOString()}`,
74
+ `Schedule: ${job.schedule}`,
75
+ `Model: ${job.model}`,
76
+ error ? `Error: ${error.message}` : 'Status: success',
77
+ `---`,
78
+ output,
79
+ ].join('\n');
80
+ fs.writeFileSync(logFile, content, 'utf-8');
81
+ }
82
+ catch {
83
+ // Logging failure shouldn't crash the daemon
84
+ }
85
+ }
86
+ }
87
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"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"}
package/install.sh ADDED
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ PLIST_NAME="com.cron-claude.daemon"
5
+ PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_NAME}.plist"
6
+ INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ LOG_DIR="$HOME/.cron-claude/logs"
8
+
9
+ mkdir -p "$HOME/Library/LaunchAgents"
10
+ mkdir -p "$LOG_DIR"
11
+
12
+ cat > "$PLIST_PATH" << EOF
13
+ <?xml version="1.0" encoding="UTF-8"?>
14
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
15
+ <plist version="1.0">
16
+ <dict>
17
+ <key>Label</key>
18
+ <string>${PLIST_NAME}</string>
19
+ <key>ProgramArguments</key>
20
+ <array>
21
+ <string>$(which node)</string>
22
+ <string>${INSTALL_DIR}/dist/daemon.js</string>
23
+ </array>
24
+ <key>RunAtLoad</key>
25
+ <true/>
26
+ <key>KeepAlive</key>
27
+ <true/>
28
+ <key>StandardOutPath</key>
29
+ <string>${LOG_DIR}/daemon-stdout.log</string>
30
+ <key>StandardErrorPath</key>
31
+ <string>${LOG_DIR}/daemon-stderr.log</string>
32
+ <key>WorkingDirectory</key>
33
+ <string>${INSTALL_DIR}</string>
34
+ <key>EnvironmentVariables</key>
35
+ <dict>
36
+ <key>PATH</key>
37
+ <string>/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin</string>
38
+ <key>HOME</key>
39
+ <string>${HOME}</string>
40
+ </dict>
41
+ </dict>
42
+ </plist>
43
+ EOF
44
+
45
+ echo "LaunchAgent plist written to: $PLIST_PATH"
46
+
47
+ # Unload if already loaded
48
+ launchctl unload "$PLIST_PATH" 2>/dev/null || true
49
+
50
+ # Load the agent
51
+ launchctl load "$PLIST_PATH"
52
+
53
+ echo "cron-claude daemon installed and started."
54
+ echo "To stop: launchctl unload $PLIST_PATH"
55
+ echo "To restart: launchctl unload $PLIST_PATH && launchctl load $PLIST_PATH"
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "cron-claude",
3
+ "version": "1.0.0",
4
+ "description": "Schedule any Claude prompt to run automatically on a cron. Lightweight alternative to a full AI platform for recurring agentic tasks.",
5
+ "type": "module",
6
+ "main": "dist/daemon.js",
7
+ "bin": {
8
+ "cron-claude": "dist/cli.js",
9
+ "cron-claude-mcp": "dist/mcp-server.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsx src/daemon.ts",
14
+ "start": "node dist/daemon.js",
15
+ "cli": "tsx src/cli.ts",
16
+ "test": "vitest run --coverage",
17
+ "test:watch": "vitest",
18
+ "install-service": "bash install.sh",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "files": [
22
+ "dist/",
23
+ "install.sh",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "keywords": [
28
+ "claude",
29
+ "cron",
30
+ "mcp",
31
+ "ai",
32
+ "automation",
33
+ "scheduler",
34
+ "anthropic",
35
+ "claude-code"
36
+ ],
37
+ "author": "tygi",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/tygiacalone/cron-claude.git"
42
+ },
43
+ "homepage": "https://github.com/tygiacalone/cron-claude#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/tygiacalone/cron-claude/issues"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.26.0",
52
+ "commander": "^12.1.0",
53
+ "node-cron": "^3.0.3",
54
+ "zod": "^3.23.8"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^22.0.0",
58
+ "@types/node-cron": "^3.0.11",
59
+ "@vitest/coverage-v8": "^2.1.0",
60
+ "tsx": "^4.19.0",
61
+ "typescript": "^5.6.0",
62
+ "vitest": "^2.1.0"
63
+ }
64
+ }