@mrsekut/ccron 0.1.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 +75 -0
- package/package.json +35 -0
- package/skills/ccron/SKILL.md +91 -0
- package/src/cli.ts +92 -0
- package/src/commands/add.ts +251 -0
- package/src/commands/auth.ts +67 -0
- package/src/commands/edit.ts +158 -0
- package/src/commands/list.ts +58 -0
- package/src/commands/log.ts +61 -0
- package/src/commands/remove.ts +94 -0
- package/src/commands/run.ts +61 -0
- package/src/commands/show.ts +91 -0
- package/src/commands/test.ts +223 -0
- package/src/config.ts +141 -0
- package/src/generator.ts +170 -0
- package/src/launchd.ts +61 -0
- package/src/mcp.ts +49 -0
- package/src/schedule.ts +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mrsekut
|
|
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,75 @@
|
|
|
1
|
+
# ccron
|
|
2
|
+
|
|
3
|
+
Schedule `claude -p` execution on macOS with launchd.
|
|
4
|
+
|
|
5
|
+
Register scheduled `claude -p` tasks with a single command. Handles all the tricky launchd setup automatically.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Run directly with `bunx`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bunx @mrsekut/ccron <command> [options]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Claude Code Skill
|
|
16
|
+
|
|
17
|
+
Install the ccron skill so Claude Code can register tasks from natural language:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bunx skills add mrsekut/ccron
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then tell Claude: "Schedule a daily summary to Slack at 5pm on weekdays" and it will handle the rest.
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Register a task
|
|
29
|
+
ccron add \
|
|
30
|
+
--name daily-summary \
|
|
31
|
+
--schedule "15 17 * * 1-5" \
|
|
32
|
+
--prompt-file ./prompts/daily-summary.txt \
|
|
33
|
+
--mcp slack
|
|
34
|
+
|
|
35
|
+
# Verify setup
|
|
36
|
+
ccron test daily-summary
|
|
37
|
+
|
|
38
|
+
# List all tasks
|
|
39
|
+
ccron list
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
| Command | Description |
|
|
45
|
+
| --------------------- | ---------------------------------- |
|
|
46
|
+
| `ccron add` | Register a new scheduled task |
|
|
47
|
+
| `ccron list` | List tasks with launchd status |
|
|
48
|
+
| `ccron show <name>` | Show detailed task info |
|
|
49
|
+
| `ccron run <name>` | Manually trigger and tail log |
|
|
50
|
+
| `ccron test <name>` | Run environment checks |
|
|
51
|
+
| `ccron log <name>` | Show logs (`--follow` for tail) |
|
|
52
|
+
| `ccron edit <name>` | Edit config, regenerate and reload |
|
|
53
|
+
| `ccron auth <name>` | Re-authenticate MCP servers |
|
|
54
|
+
| `ccron remove <name>` | Remove task (`--purge` for logs) |
|
|
55
|
+
|
|
56
|
+
## Schedule Format
|
|
57
|
+
|
|
58
|
+
Standard cron expression: `"minute hour * * day-of-week"`
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
15 17 * * * Every day at 17:15
|
|
62
|
+
15 17 * * 1-5 Weekdays at 17:15
|
|
63
|
+
0 22 * * 5 Every Friday at 22:00
|
|
64
|
+
0 9 * * 1,3,5 Mon/Wed/Fri at 9:00
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Step values (`*/5`) and minute/hour ranges are not supported (launchd limitation).
|
|
68
|
+
|
|
69
|
+
## MCP Presets
|
|
70
|
+
|
|
71
|
+
`slack`, `linear` are available as built-in presets.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ccron add --name my-task --schedule "0 9 * * *" --prompt "hello" --mcp slack,linear
|
|
75
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrsekut/ccron",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Schedule claude -p execution on macOS with launchd",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "mrsekut",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/mrsekut/ccron.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"launchd",
|
|
15
|
+
"cron",
|
|
16
|
+
"schedule",
|
|
17
|
+
"macos",
|
|
18
|
+
"claude-code"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"ccron": "src/cli.ts"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"!src/**/*.test.ts",
|
|
26
|
+
"skills"
|
|
27
|
+
],
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/bun": "latest",
|
|
30
|
+
"prettier": "^3.8.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"typescript": "^5"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ccron
|
|
3
|
+
description: >
|
|
4
|
+
Schedule claude -p execution on macOS with launchd. Use when the user wants to
|
|
5
|
+
set up recurring claude tasks, manage scheduled prompts, or automate claude CLI runs.
|
|
6
|
+
allowed-tools: 'Read,Write,Bash(ccron:*)'
|
|
7
|
+
version: '0.1.0'
|
|
8
|
+
author: 'mrsekut'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ccron - Schedule claude -p on macOS with launchd
|
|
12
|
+
|
|
13
|
+
Schedule `claude -p` on macOS via launchd. Handles all the tricky launchd setup automatically.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
When a user asks to run something on a schedule:
|
|
18
|
+
|
|
19
|
+
1. Create a prompt file (`./prompts/<name>.txt` or similar, git-manageable location)
|
|
20
|
+
2. Register with `ccron add`
|
|
21
|
+
3. Verify with `ccron test <name>`
|
|
22
|
+
|
|
23
|
+
**Always run `ccron <command> --help` to check the latest options.**
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
| Command | Purpose |
|
|
28
|
+
| --------------------- | ------------------------------------ |
|
|
29
|
+
| `ccron add` | Register a scheduled task |
|
|
30
|
+
| `ccron list` | List tasks with launchd status |
|
|
31
|
+
| `ccron show <name>` | Show detailed task info |
|
|
32
|
+
| `ccron run <name>` | Manually trigger and tail log |
|
|
33
|
+
| `ccron test <name>` | Run environment checks |
|
|
34
|
+
| `ccron log <name>` | Show logs (`--follow` for tail) |
|
|
35
|
+
| `ccron auth <name>` | Re-authenticate MCP servers |
|
|
36
|
+
| `ccron edit <name>` | Edit config, regenerate and reload |
|
|
37
|
+
| `ccron remove <name>` | Remove task (`--purge` for logs too) |
|
|
38
|
+
|
|
39
|
+
## ccron add Options
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
--name <name> Task name (lowercase, numbers, hyphens)
|
|
43
|
+
--schedule "<cron>" Cron expression: "minute hour * * day-of-week"
|
|
44
|
+
--prompt "<text>" Prompt string (mutually exclusive with --prompt-file)
|
|
45
|
+
--prompt-file <path> Prompt file path (mutually exclusive with --prompt)
|
|
46
|
+
--mcp <names> MCP presets, comma-separated (slack, linear)
|
|
47
|
+
--allowed-tools <tools> Allowed tools (Bash,Read,Write,Edit,Glob,Grep)
|
|
48
|
+
--max-turns <n> Maximum turns
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Schedule (cron)
|
|
52
|
+
|
|
53
|
+
Format: `"minute hour * * day-of-week"` (5 fields)
|
|
54
|
+
|
|
55
|
+
| Goal | Cron |
|
|
56
|
+
| --------------------- | ----------------- |
|
|
57
|
+
| Daily at 17:15 | `"15 17 * * *"` |
|
|
58
|
+
| Weekdays at 17:15 | `"15 17 * * 1-5"` |
|
|
59
|
+
| Every Friday at 22:00 | `"0 22 * * 5"` |
|
|
60
|
+
| Mon/Wed/Fri at 9:00 | `"0 9 * * 1,3,5"` |
|
|
61
|
+
|
|
62
|
+
**Constraint**: Step values (`*/5`) and minute/hour ranges are not supported (launchd limitation).
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
User: "Post a daily summary to Slack at 5pm on weekdays"
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 1. Create prompt file
|
|
70
|
+
cat > ./prompts/daily-summary.txt << 'EOF'
|
|
71
|
+
Create a daily summary and post it to #daily-summary channel.
|
|
72
|
+
Include: completed tasks, tomorrow's plan, blockers.
|
|
73
|
+
EOF
|
|
74
|
+
|
|
75
|
+
# 2. Register
|
|
76
|
+
ccron add \
|
|
77
|
+
--name daily-summary \
|
|
78
|
+
--schedule "0 17 * * 1-5" \
|
|
79
|
+
--prompt-file ./prompts/daily-summary.txt \
|
|
80
|
+
--mcp slack
|
|
81
|
+
|
|
82
|
+
# 3. Verify
|
|
83
|
+
ccron test daily-summary
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Troubleshooting
|
|
87
|
+
|
|
88
|
+
- `ccron test` fails: follow the suggested fix commands in the output
|
|
89
|
+
- MCP auth expired: `ccron auth <name>`
|
|
90
|
+
- Prompt change: just edit the file if using `--prompt-file` (no re-registration needed)
|
|
91
|
+
- Schedule change: `ccron edit <name> --schedule "<cron>"`
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const command = args[0];
|
|
5
|
+
|
|
6
|
+
function printHelp() {
|
|
7
|
+
console.log(`ccron - Schedule claude -p execution on macOS with launchd
|
|
8
|
+
|
|
9
|
+
Schedule claude -p execution on macOS via launchd.
|
|
10
|
+
Handles all the tricky launchd setup automatically.
|
|
11
|
+
|
|
12
|
+
Usage: ccron <command> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
add Register a new scheduled task
|
|
16
|
+
list List all registered tasks with launchd status
|
|
17
|
+
show Show detailed information about a task
|
|
18
|
+
run Manually trigger a task and tail its log
|
|
19
|
+
test Run environment checks (CLI, auth, ulimit, TCC, plist)
|
|
20
|
+
log Show task execution logs
|
|
21
|
+
auth Re-authenticate MCP servers for a task
|
|
22
|
+
edit Edit a task's configuration (regenerates script/plist)
|
|
23
|
+
remove Remove a task and all generated files
|
|
24
|
+
|
|
25
|
+
Schedule format (cron expression):
|
|
26
|
+
"minute hour * * day-of-week"
|
|
27
|
+
|
|
28
|
+
15 17 * * * Every day at 17:15
|
|
29
|
+
15 17 * * 1-5 Weekdays at 17:15
|
|
30
|
+
0 22 * * 5 Every Friday at 22:00
|
|
31
|
+
0 9 * * 1,3,5 Mon/Wed/Fri at 9:00
|
|
32
|
+
|
|
33
|
+
Note: Step values (*/5) and minute/hour ranges are not supported (launchd limitation).
|
|
34
|
+
|
|
35
|
+
MCP presets: slack, linear
|
|
36
|
+
|
|
37
|
+
File locations:
|
|
38
|
+
Task configs: ~/.config/ccron/tasks/<name>.json
|
|
39
|
+
MCP configs: ~/.config/ccron/mcp/<name>.json
|
|
40
|
+
Scripts: ~/.local/bin/ccron-<name>.sh
|
|
41
|
+
Plists: ~/Library/LaunchAgents/com.ccron.<name>.plist
|
|
42
|
+
Logs: ~/.local/share/ccron/logs/<name>.log
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
ccron add --name daily-summary --schedule "15 17 * * 1-5" --prompt-file ./prompts/daily.txt
|
|
46
|
+
ccron add --name weekly-review --schedule "0 22 * * 5" --prompt "週次レビュー" --mcp slack
|
|
47
|
+
ccron list
|
|
48
|
+
ccron show daily-summary
|
|
49
|
+
ccron test daily-summary
|
|
50
|
+
ccron edit daily-summary --schedule "0 18 * * 1-5"
|
|
51
|
+
ccron run daily-summary
|
|
52
|
+
ccron log daily-summary --follow
|
|
53
|
+
ccron remove daily-summary
|
|
54
|
+
|
|
55
|
+
Run "ccron <command> --help" for detailed help on each command.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!command || command === '--help' || command === '-h') {
|
|
59
|
+
printHelp();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
import { addCommand } from './commands/add';
|
|
64
|
+
import { listCommand } from './commands/list';
|
|
65
|
+
import { runCommand } from './commands/run';
|
|
66
|
+
import { logCommand } from './commands/log';
|
|
67
|
+
import { testCommand } from './commands/test';
|
|
68
|
+
import { removeCommand } from './commands/remove';
|
|
69
|
+
import { editCommand } from './commands/edit';
|
|
70
|
+
import { authCommand } from './commands/auth';
|
|
71
|
+
import { showCommand } from './commands/show';
|
|
72
|
+
|
|
73
|
+
const commands: Record<string, (args: string[]) => Promise<void>> = {
|
|
74
|
+
add: addCommand,
|
|
75
|
+
list: listCommand,
|
|
76
|
+
show: showCommand,
|
|
77
|
+
run: runCommand,
|
|
78
|
+
log: logCommand,
|
|
79
|
+
test: testCommand,
|
|
80
|
+
remove: removeCommand,
|
|
81
|
+
edit: editCommand,
|
|
82
|
+
auth: authCommand,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handler = commands[command];
|
|
86
|
+
if (!handler) {
|
|
87
|
+
console.error(`Unknown command: ${command}`);
|
|
88
|
+
console.error('Run "ccron --help" for usage.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await handler(args.slice(1));
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type TaskConfig,
|
|
3
|
+
type GlobalConfig,
|
|
4
|
+
validateTaskName,
|
|
5
|
+
readTaskConfig,
|
|
6
|
+
writeTaskConfig,
|
|
7
|
+
readGlobalConfig,
|
|
8
|
+
writeGlobalConfig,
|
|
9
|
+
ensureDirectories,
|
|
10
|
+
mcpConfigPath,
|
|
11
|
+
scriptPath,
|
|
12
|
+
plistPath,
|
|
13
|
+
} from '../config';
|
|
14
|
+
import { parseSchedule } from '../schedule';
|
|
15
|
+
import { validateMcpPresets } from '../mcp';
|
|
16
|
+
import {
|
|
17
|
+
generateScriptContent,
|
|
18
|
+
generatePlistContent,
|
|
19
|
+
generateMcpConfigContent,
|
|
20
|
+
} from '../generator';
|
|
21
|
+
import { bootstrap } from '../launchd';
|
|
22
|
+
|
|
23
|
+
type AddOptions = {
|
|
24
|
+
name: string;
|
|
25
|
+
schedule: string;
|
|
26
|
+
prompt: string | null;
|
|
27
|
+
promptFile: string | null;
|
|
28
|
+
mcp: string[];
|
|
29
|
+
allowedTools: string[];
|
|
30
|
+
maxTurns: number | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export async function addCommand(args: string[]): Promise<void> {
|
|
34
|
+
const opts = parseAddArgs(args);
|
|
35
|
+
await validateOptions(opts);
|
|
36
|
+
|
|
37
|
+
await ensureDirectories();
|
|
38
|
+
|
|
39
|
+
// Detect claude CLI path if not configured
|
|
40
|
+
const globalConfig = await ensureGlobalConfig();
|
|
41
|
+
|
|
42
|
+
// Parse schedule to validate it early
|
|
43
|
+
const intervals = parseSchedule(opts.schedule);
|
|
44
|
+
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
const task: TaskConfig = {
|
|
47
|
+
name: opts.name,
|
|
48
|
+
schedule: opts.schedule,
|
|
49
|
+
prompt: opts.prompt,
|
|
50
|
+
promptFile: opts.promptFile,
|
|
51
|
+
mcp: opts.mcp,
|
|
52
|
+
allowedTools: opts.allowedTools,
|
|
53
|
+
maxTurns: opts.maxTurns,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Save task config
|
|
59
|
+
await writeTaskConfig(task);
|
|
60
|
+
console.log(`✓ Task config saved: ~/.config/ccron/tasks/${task.name}.json`);
|
|
61
|
+
|
|
62
|
+
// Generate MCP config if needed
|
|
63
|
+
if (task.mcp.length > 0) {
|
|
64
|
+
const mcpContent = generateMcpConfigContent(task.mcp);
|
|
65
|
+
await Bun.write(mcpConfigPath(task.name), mcpContent);
|
|
66
|
+
console.log(`✓ MCP config saved: ~/.config/ccron/mcp/${task.name}.json`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate script
|
|
70
|
+
const scriptContent = generateScriptContent(task, globalConfig);
|
|
71
|
+
const scriptFile = scriptPath(task.name);
|
|
72
|
+
await Bun.write(scriptFile, scriptContent);
|
|
73
|
+
const { chmod } = await import('fs/promises');
|
|
74
|
+
await chmod(scriptFile, 0o755);
|
|
75
|
+
console.log(`✓ Script generated: ~/.local/bin/ccron-${task.name}.sh`);
|
|
76
|
+
|
|
77
|
+
// Generate plist
|
|
78
|
+
const plistContent = generatePlistContent(task, globalConfig, intervals);
|
|
79
|
+
const plistFile = plistPath(task.name);
|
|
80
|
+
await Bun.write(plistFile, plistContent);
|
|
81
|
+
console.log(
|
|
82
|
+
`✓ Plist generated: ~/Library/LaunchAgents/com.ccron.${task.name}.plist`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Register with launchd
|
|
86
|
+
await bootstrap(plistFile);
|
|
87
|
+
console.log(`✓ Registered with launchd`);
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
`\n${task.name} scheduled (${task.schedule}). Run "ccron test ${task.name}" to verify setup.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseAddArgs(args: string[]): AddOptions {
|
|
95
|
+
const { parseArgs } = require('util');
|
|
96
|
+
const { values } = parseArgs({
|
|
97
|
+
args,
|
|
98
|
+
options: {
|
|
99
|
+
name: { type: 'string' },
|
|
100
|
+
schedule: { type: 'string' },
|
|
101
|
+
prompt: { type: 'string' },
|
|
102
|
+
'prompt-file': { type: 'string' },
|
|
103
|
+
mcp: { type: 'string' },
|
|
104
|
+
'allowed-tools': { type: 'string' },
|
|
105
|
+
'max-turns': { type: 'string' },
|
|
106
|
+
help: { type: 'boolean', short: 'h' },
|
|
107
|
+
},
|
|
108
|
+
strict: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (values.help) {
|
|
112
|
+
printAddHelp();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
name: values.name ?? '',
|
|
118
|
+
schedule: values.schedule ?? '',
|
|
119
|
+
prompt: values.prompt ?? null,
|
|
120
|
+
promptFile: values['prompt-file'] ?? null,
|
|
121
|
+
mcp: values.mcp ? values.mcp.split(',').filter(Boolean) : [],
|
|
122
|
+
allowedTools: values['allowed-tools']
|
|
123
|
+
? values['allowed-tools'].split(',').filter(Boolean)
|
|
124
|
+
: [],
|
|
125
|
+
maxTurns: values['max-turns'] ? Number(values['max-turns']) : null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function validateOptions(opts: AddOptions): Promise<void> {
|
|
130
|
+
const errors: string[] = [];
|
|
131
|
+
|
|
132
|
+
// Name
|
|
133
|
+
if (!opts.name) {
|
|
134
|
+
errors.push('--name is required');
|
|
135
|
+
} else {
|
|
136
|
+
const nameError = validateTaskName(opts.name);
|
|
137
|
+
if (nameError) errors.push(nameError);
|
|
138
|
+
else {
|
|
139
|
+
const existing = await readTaskConfig(opts.name);
|
|
140
|
+
if (existing) {
|
|
141
|
+
errors.push(
|
|
142
|
+
`Task "${opts.name}" already exists. Use "ccron edit ${opts.name}" to modify.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Schedule
|
|
149
|
+
if (!opts.schedule) {
|
|
150
|
+
errors.push('--schedule is required');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Prompt
|
|
154
|
+
if (!opts.prompt && !opts.promptFile) {
|
|
155
|
+
errors.push('Either --prompt or --prompt-file is required');
|
|
156
|
+
}
|
|
157
|
+
if (opts.prompt && opts.promptFile) {
|
|
158
|
+
errors.push('Cannot specify both --prompt and --prompt-file');
|
|
159
|
+
}
|
|
160
|
+
if (opts.promptFile) {
|
|
161
|
+
const file = Bun.file(opts.promptFile);
|
|
162
|
+
if (!(await file.exists())) {
|
|
163
|
+
errors.push(`Prompt file not found: ${opts.promptFile}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// MCP
|
|
168
|
+
if (opts.mcp.length > 0) {
|
|
169
|
+
const mcpError = validateMcpPresets(opts.mcp);
|
|
170
|
+
if (mcpError) errors.push(mcpError);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Max turns
|
|
174
|
+
if (
|
|
175
|
+
opts.maxTurns !== null &&
|
|
176
|
+
(!Number.isInteger(opts.maxTurns) || opts.maxTurns < 1)
|
|
177
|
+
) {
|
|
178
|
+
errors.push('--max-turns must be a positive integer');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (errors.length > 0) {
|
|
182
|
+
for (const e of errors) console.error(`Error: ${e}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function ensureGlobalConfig(): Promise<GlobalConfig> {
|
|
188
|
+
const config = await readGlobalConfig();
|
|
189
|
+
|
|
190
|
+
if (!config.claudePath) {
|
|
191
|
+
const detected = await detectClaudePath();
|
|
192
|
+
if (detected) {
|
|
193
|
+
config.claudePath = detected;
|
|
194
|
+
await writeGlobalConfig(config);
|
|
195
|
+
console.log(`✓ claude CLI detected: ${detected}`);
|
|
196
|
+
} else {
|
|
197
|
+
console.error(
|
|
198
|
+
'Warning: Could not detect claude CLI path. Set it manually with ccron config --claude-path',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return config;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function detectClaudePath(): Promise<string | null> {
|
|
207
|
+
try {
|
|
208
|
+
const result = await Bun.$`which claude`.quiet();
|
|
209
|
+
if (result.exitCode === 0) {
|
|
210
|
+
const fullPath = result.stdout.toString().trim();
|
|
211
|
+
// Return the directory containing claude
|
|
212
|
+
const { dirname } = await import('path');
|
|
213
|
+
return dirname(fullPath);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// ignore
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function printAddHelp() {
|
|
222
|
+
console.log(`ccron add - Register a new scheduled task
|
|
223
|
+
|
|
224
|
+
Usage: ccron add [options]
|
|
225
|
+
|
|
226
|
+
Required:
|
|
227
|
+
--name <name> Task name (lowercase letters, numbers, hyphens)
|
|
228
|
+
--schedule <cron> Cron expression: "minute hour * * day-of-week"
|
|
229
|
+
--prompt <text> Prompt string (mutually exclusive with --prompt-file)
|
|
230
|
+
--prompt-file <path> Path to prompt file (mutually exclusive with --prompt)
|
|
231
|
+
|
|
232
|
+
Optional:
|
|
233
|
+
--mcp <names> MCP server presets, comma-separated (slack, linear)
|
|
234
|
+
--allowed-tools <tools> Allowed tools, comma-separated (Bash,Read,Write,Edit,Glob,Grep)
|
|
235
|
+
--max-turns <n> Maximum number of turns
|
|
236
|
+
|
|
237
|
+
Schedule format (cron):
|
|
238
|
+
"minute hour * * day-of-week"
|
|
239
|
+
|
|
240
|
+
minute: 0-59
|
|
241
|
+
hour: 0-23
|
|
242
|
+
day-of-week: 0-6 (0=Sun), ranges (1-5), comma-separated (1,3,5)
|
|
243
|
+
day-of-month and month: must be "*" (launchd limitation)
|
|
244
|
+
|
|
245
|
+
Examples:
|
|
246
|
+
ccron add --name daily-summary --schedule "15 17 * * 1-5" --prompt-file ./prompts/daily.txt
|
|
247
|
+
ccron add --name weekly-review --schedule "0 22 * * 5" --prompt "週次レビューを作成して" --mcp slack
|
|
248
|
+
ccron add --name hourly-check --schedule "0 9 * * *" --prompt "ステータスチェック" --allowed-tools "Bash,Read"
|
|
249
|
+
|
|
250
|
+
Note: Step values (*/5) and ranges in minute/hour are not supported by launchd.`);
|
|
251
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readTaskConfig, mcpConfigPath } from '../config';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
export async function authCommand(args: string[]): Promise<void> {
|
|
5
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
6
|
+
console.log(`ccron auth - Re-authenticate MCP servers for a task
|
|
7
|
+
|
|
8
|
+
Usage: ccron auth <name>
|
|
9
|
+
|
|
10
|
+
Opens an interactive claude session with the task's MCP config, allowing
|
|
11
|
+
you to complete OAuth flows for MCP servers (Slack, Linear, etc.).
|
|
12
|
+
The session runs from /tmp to avoid git repo issues.
|
|
13
|
+
|
|
14
|
+
Exit claude after authentication is complete.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
ccron auth daily-summary`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const name = args[0];
|
|
22
|
+
if (!name) {
|
|
23
|
+
console.error('Usage: ccron auth <name>');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const task = await readTaskConfig(name);
|
|
28
|
+
if (!task) {
|
|
29
|
+
console.error(`Task "${name}" not found.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (task.mcp.length === 0) {
|
|
34
|
+
console.log(`Task "${name}" has no MCP servers configured.`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mcpConfig = mcpConfigPath(name);
|
|
39
|
+
if (!(await Bun.file(mcpConfig).exists())) {
|
|
40
|
+
console.error(`MCP config not found: ${mcpConfig}`);
|
|
41
|
+
console.error(
|
|
42
|
+
`Re-register with: ccron add --name ${name} --mcp ${task.mcp.join(',')}`,
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`Opening claude with MCP config for "${name}"...`);
|
|
48
|
+
console.log(`MCP servers: ${task.mcp.join(', ')}`);
|
|
49
|
+
console.log(`Complete the authentication flow, then exit claude.\n`);
|
|
50
|
+
|
|
51
|
+
const child = spawn('claude', ['--mcp-config', mcpConfig], {
|
|
52
|
+
cwd: '/tmp',
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await new Promise<void>((resolve, reject) => {
|
|
57
|
+
child.on('close', code => {
|
|
58
|
+
if (code === 0) {
|
|
59
|
+
console.log(`\nAuthentication complete for "${name}".`);
|
|
60
|
+
resolve();
|
|
61
|
+
} else {
|
|
62
|
+
reject(new Error(`claude exited with code ${code}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
child.on('error', reject);
|
|
66
|
+
});
|
|
67
|
+
}
|