@mrsekut/ccron 0.1.0 → 0.2.1
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/README.md +6 -6
- package/package.json +1 -1
- package/skills/ccron/SKILL.md +25 -25
- package/src/cli.ts +2 -5
- package/src/commands/add.ts +15 -58
- package/src/commands/auth.ts +8 -12
- package/src/commands/edit.ts +10 -50
- package/src/commands/remove.ts +8 -34
- package/src/commands/show.ts +3 -11
- package/src/commands/test.ts +54 -8
- package/src/config.ts +1 -10
- package/src/generator.ts +4 -22
- package/src/mcp.ts +0 -49
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ Then tell Claude: "Schedule a daily summary to Slack at 5pm on weekdays" and it
|
|
|
29
29
|
ccron add \
|
|
30
30
|
--name daily-summary \
|
|
31
31
|
--schedule "15 17 * * 1-5" \
|
|
32
|
-
--prompt
|
|
33
|
-
--mcp
|
|
32
|
+
--prompt "日次サマリーを作成して #daily-summary チャンネルに投稿して" \
|
|
33
|
+
--mcp-config ~/mcp.json
|
|
34
34
|
|
|
35
35
|
# Verify setup
|
|
36
36
|
ccron test daily-summary
|
|
@@ -51,7 +51,7 @@ ccron list
|
|
|
51
51
|
| `ccron log <name>` | Show logs (`--follow` for tail) |
|
|
52
52
|
| `ccron edit <name>` | Edit config, regenerate and reload |
|
|
53
53
|
| `ccron auth <name>` | Re-authenticate MCP servers |
|
|
54
|
-
| `ccron remove <name>` | Remove task
|
|
54
|
+
| `ccron remove <name>` | Remove task and logs |
|
|
55
55
|
|
|
56
56
|
## Schedule Format
|
|
57
57
|
|
|
@@ -66,10 +66,10 @@ Standard cron expression: `"minute hour * * day-of-week"`
|
|
|
66
66
|
|
|
67
67
|
Step values (`*/5`) and minute/hour ranges are not supported (launchd limitation).
|
|
68
68
|
|
|
69
|
-
## MCP
|
|
69
|
+
## MCP
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Pass your own MCP config JSON file via `--mcp-config`:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
ccron add --name my-task --schedule "0 9 * * *" --prompt "hello" --mcp
|
|
74
|
+
ccron add --name my-task --schedule "0 9 * * *" --prompt "hello" --mcp-config ~/mcp.json
|
|
75
75
|
```
|
package/package.json
CHANGED
package/skills/ccron/SKILL.md
CHANGED
|
@@ -16,36 +16,33 @@ Schedule `claude -p` on macOS via launchd. Handles all the tricky launchd setup
|
|
|
16
16
|
|
|
17
17
|
When a user asks to run something on a schedule:
|
|
18
18
|
|
|
19
|
-
1.
|
|
20
|
-
2. Register with `ccron add`
|
|
19
|
+
1. If MCP servers are needed, write the MCP config JSON file first
|
|
20
|
+
2. Register with `ccron add --prompt "<prompt text>"`
|
|
21
21
|
3. Verify with `ccron test <name>`
|
|
22
22
|
|
|
23
23
|
**Always run `ccron <command> --help` to check the latest options.**
|
|
24
24
|
|
|
25
25
|
## Commands
|
|
26
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
|
|
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 and logs |
|
|
38
38
|
|
|
39
39
|
## ccron add Options
|
|
40
40
|
|
|
41
41
|
```
|
|
42
42
|
--name <name> Task name (lowercase, numbers, hyphens)
|
|
43
43
|
--schedule "<cron>" Cron expression: "minute hour * * day-of-week"
|
|
44
|
-
--prompt "<text>" Prompt string
|
|
45
|
-
--
|
|
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
|
|
44
|
+
--prompt "<text>" Prompt string
|
|
45
|
+
--mcp-config <path> Path to MCP config JSON file
|
|
49
46
|
```
|
|
50
47
|
|
|
51
48
|
## Schedule (cron)
|
|
@@ -66,18 +63,21 @@ Format: `"minute hour * * day-of-week"` (5 fields)
|
|
|
66
63
|
User: "Post a daily summary to Slack at 5pm on weekdays"
|
|
67
64
|
|
|
68
65
|
```bash
|
|
69
|
-
# 1.
|
|
70
|
-
cat >
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
# 1. Write MCP config
|
|
67
|
+
cat > ~/.config/ccron/mcp-slack.json << 'EOF'
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"slack": { "type": "http", "url": "https://mcp.slack.com/mcp" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
73
|
EOF
|
|
74
74
|
|
|
75
75
|
# 2. Register
|
|
76
76
|
ccron add \
|
|
77
77
|
--name daily-summary \
|
|
78
78
|
--schedule "0 17 * * 1-5" \
|
|
79
|
-
--prompt
|
|
80
|
-
--mcp slack
|
|
79
|
+
--prompt "Create a daily summary and post it to #daily-summary channel. Include: completed tasks, tomorrow's plan, blockers." \
|
|
80
|
+
--mcp-config ~/.config/ccron/mcp-slack.json
|
|
81
81
|
|
|
82
82
|
# 3. Verify
|
|
83
83
|
ccron test daily-summary
|
|
@@ -87,5 +87,5 @@ ccron test daily-summary
|
|
|
87
87
|
|
|
88
88
|
- `ccron test` fails: follow the suggested fix commands in the output
|
|
89
89
|
- MCP auth expired: `ccron auth <name>`
|
|
90
|
-
- Prompt change:
|
|
90
|
+
- Prompt change: `ccron edit <name> --prompt "<new prompt>"`
|
|
91
91
|
- Schedule change: `ccron edit <name> --schedule "<cron>"`
|
package/src/cli.ts
CHANGED
|
@@ -32,18 +32,15 @@ Schedule format (cron expression):
|
|
|
32
32
|
|
|
33
33
|
Note: Step values (*/5) and minute/hour ranges are not supported (launchd limitation).
|
|
34
34
|
|
|
35
|
-
MCP presets: slack, linear
|
|
36
|
-
|
|
37
35
|
File locations:
|
|
38
36
|
Task configs: ~/.config/ccron/tasks/<name>.json
|
|
39
|
-
MCP configs: ~/.config/ccron/mcp/<name>.json
|
|
40
37
|
Scripts: ~/.local/bin/ccron-<name>.sh
|
|
41
38
|
Plists: ~/Library/LaunchAgents/com.ccron.<name>.plist
|
|
42
39
|
Logs: ~/.local/share/ccron/logs/<name>.log
|
|
43
40
|
|
|
44
41
|
Examples:
|
|
45
|
-
ccron add --name daily-summary --schedule "15 17 * * 1-5" --prompt
|
|
46
|
-
ccron add --name weekly-review --schedule "0 22 * * 5" --prompt "週次レビュー" --mcp
|
|
42
|
+
ccron add --name daily-summary --schedule "15 17 * * 1-5" --prompt "日次サマリーを作成して"
|
|
43
|
+
ccron add --name weekly-review --schedule "0 22 * * 5" --prompt "週次レビュー" --mcp-config ~/mcp.json
|
|
47
44
|
ccron list
|
|
48
45
|
ccron show daily-summary
|
|
49
46
|
ccron test daily-summary
|
package/src/commands/add.ts
CHANGED
|
@@ -7,16 +7,13 @@ import {
|
|
|
7
7
|
readGlobalConfig,
|
|
8
8
|
writeGlobalConfig,
|
|
9
9
|
ensureDirectories,
|
|
10
|
-
mcpConfigPath,
|
|
11
10
|
scriptPath,
|
|
12
11
|
plistPath,
|
|
13
12
|
} from '../config';
|
|
14
13
|
import { parseSchedule } from '../schedule';
|
|
15
|
-
import { validateMcpPresets } from '../mcp';
|
|
16
14
|
import {
|
|
17
15
|
generateScriptContent,
|
|
18
16
|
generatePlistContent,
|
|
19
|
-
generateMcpConfigContent,
|
|
20
17
|
} from '../generator';
|
|
21
18
|
import { bootstrap } from '../launchd';
|
|
22
19
|
|
|
@@ -24,10 +21,7 @@ type AddOptions = {
|
|
|
24
21
|
name: string;
|
|
25
22
|
schedule: string;
|
|
26
23
|
prompt: string | null;
|
|
27
|
-
|
|
28
|
-
mcp: string[];
|
|
29
|
-
allowedTools: string[];
|
|
30
|
-
maxTurns: number | null;
|
|
24
|
+
mcpConfig: string | null;
|
|
31
25
|
};
|
|
32
26
|
|
|
33
27
|
export async function addCommand(args: string[]): Promise<void> {
|
|
@@ -47,10 +41,7 @@ export async function addCommand(args: string[]): Promise<void> {
|
|
|
47
41
|
name: opts.name,
|
|
48
42
|
schedule: opts.schedule,
|
|
49
43
|
prompt: opts.prompt,
|
|
50
|
-
|
|
51
|
-
mcp: opts.mcp,
|
|
52
|
-
allowedTools: opts.allowedTools,
|
|
53
|
-
maxTurns: opts.maxTurns,
|
|
44
|
+
mcpConfig: opts.mcpConfig,
|
|
54
45
|
createdAt: now,
|
|
55
46
|
updatedAt: now,
|
|
56
47
|
};
|
|
@@ -59,13 +50,6 @@ export async function addCommand(args: string[]): Promise<void> {
|
|
|
59
50
|
await writeTaskConfig(task);
|
|
60
51
|
console.log(`✓ Task config saved: ~/.config/ccron/tasks/${task.name}.json`);
|
|
61
52
|
|
|
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
53
|
// Generate script
|
|
70
54
|
const scriptContent = generateScriptContent(task, globalConfig);
|
|
71
55
|
const scriptFile = scriptPath(task.name);
|
|
@@ -99,10 +83,7 @@ function parseAddArgs(args: string[]): AddOptions {
|
|
|
99
83
|
name: { type: 'string' },
|
|
100
84
|
schedule: { type: 'string' },
|
|
101
85
|
prompt: { type: 'string' },
|
|
102
|
-
'
|
|
103
|
-
mcp: { type: 'string' },
|
|
104
|
-
'allowed-tools': { type: 'string' },
|
|
105
|
-
'max-turns': { type: 'string' },
|
|
86
|
+
'mcp-config': { type: 'string' },
|
|
106
87
|
help: { type: 'boolean', short: 'h' },
|
|
107
88
|
},
|
|
108
89
|
strict: true,
|
|
@@ -117,12 +98,7 @@ function parseAddArgs(args: string[]): AddOptions {
|
|
|
117
98
|
name: values.name ?? '',
|
|
118
99
|
schedule: values.schedule ?? '',
|
|
119
100
|
prompt: values.prompt ?? null,
|
|
120
|
-
|
|
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,
|
|
101
|
+
mcpConfig: values['mcp-config'] ?? null,
|
|
126
102
|
};
|
|
127
103
|
}
|
|
128
104
|
|
|
@@ -151,33 +127,18 @@ async function validateOptions(opts: AddOptions): Promise<void> {
|
|
|
151
127
|
}
|
|
152
128
|
|
|
153
129
|
// Prompt
|
|
154
|
-
if (!opts.prompt
|
|
155
|
-
errors.push('
|
|
130
|
+
if (!opts.prompt) {
|
|
131
|
+
errors.push('--prompt is required');
|
|
156
132
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const file = Bun.file(opts.promptFile);
|
|
133
|
+
|
|
134
|
+
// MCP config file
|
|
135
|
+
if (opts.mcpConfig) {
|
|
136
|
+
const file = Bun.file(opts.mcpConfig);
|
|
162
137
|
if (!(await file.exists())) {
|
|
163
|
-
errors.push(`
|
|
138
|
+
errors.push(`MCP config file not found: ${opts.mcpConfig}`);
|
|
164
139
|
}
|
|
165
140
|
}
|
|
166
141
|
|
|
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
142
|
if (errors.length > 0) {
|
|
182
143
|
for (const e of errors) console.error(`Error: ${e}`);
|
|
183
144
|
process.exit(1);
|
|
@@ -226,13 +187,10 @@ Usage: ccron add [options]
|
|
|
226
187
|
Required:
|
|
227
188
|
--name <name> Task name (lowercase letters, numbers, hyphens)
|
|
228
189
|
--schedule <cron> Cron expression: "minute hour * * day-of-week"
|
|
229
|
-
--prompt <text> Prompt string
|
|
230
|
-
--prompt-file <path> Path to prompt file (mutually exclusive with --prompt)
|
|
190
|
+
--prompt <text> Prompt string
|
|
231
191
|
|
|
232
192
|
Optional:
|
|
233
|
-
--mcp <
|
|
234
|
-
--allowed-tools <tools> Allowed tools, comma-separated (Bash,Read,Write,Edit,Glob,Grep)
|
|
235
|
-
--max-turns <n> Maximum number of turns
|
|
193
|
+
--mcp-config <path> Path to MCP config JSON file
|
|
236
194
|
|
|
237
195
|
Schedule format (cron):
|
|
238
196
|
"minute hour * * day-of-week"
|
|
@@ -243,9 +201,8 @@ Schedule format (cron):
|
|
|
243
201
|
day-of-month and month: must be "*" (launchd limitation)
|
|
244
202
|
|
|
245
203
|
Examples:
|
|
246
|
-
ccron add --name daily-summary --schedule "15 17 * * 1-5" --prompt
|
|
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"
|
|
204
|
+
ccron add --name daily-summary --schedule "15 17 * * 1-5" --prompt "日次サマリーを作成して"
|
|
205
|
+
ccron add --name weekly-review --schedule "0 22 * * 5" --prompt "週次レビューを作成して" --mcp-config ~/.config/ccron/mcp/slack.json
|
|
249
206
|
|
|
250
207
|
Note: Step values (*/5) and ranges in minute/hour are not supported by launchd.`);
|
|
251
208
|
}
|
package/src/commands/auth.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readTaskConfig
|
|
1
|
+
import { readTaskConfig } from '../config';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
|
|
4
4
|
export async function authCommand(args: string[]): Promise<void> {
|
|
@@ -8,7 +8,7 @@ export async function authCommand(args: string[]): Promise<void> {
|
|
|
8
8
|
Usage: ccron auth <name>
|
|
9
9
|
|
|
10
10
|
Opens an interactive claude session with the task's MCP config, allowing
|
|
11
|
-
you to complete OAuth flows for MCP servers
|
|
11
|
+
you to complete OAuth flows for MCP servers.
|
|
12
12
|
The session runs from /tmp to avoid git repo issues.
|
|
13
13
|
|
|
14
14
|
Exit claude after authentication is complete.
|
|
@@ -30,25 +30,21 @@ Example:
|
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
if (task.
|
|
34
|
-
console.log(`Task "${name}" has no MCP
|
|
33
|
+
if (!task.mcpConfig) {
|
|
34
|
+
console.log(`Task "${name}" has no MCP config.`);
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.error(`MCP config not found: ${mcpConfig}`);
|
|
41
|
-
console.error(
|
|
42
|
-
`Re-register with: ccron add --name ${name} --mcp ${task.mcp.join(',')}`,
|
|
43
|
-
);
|
|
38
|
+
if (!(await Bun.file(task.mcpConfig).exists())) {
|
|
39
|
+
console.error(`MCP config not found: ${task.mcpConfig}`);
|
|
44
40
|
process.exit(1);
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
console.log(`Opening claude with MCP config for "${name}"...`);
|
|
48
|
-
console.log(`MCP
|
|
44
|
+
console.log(`MCP config: ${task.mcpConfig}`);
|
|
49
45
|
console.log(`Complete the authentication flow, then exit claude.\n`);
|
|
50
46
|
|
|
51
|
-
const child = spawn('claude', ['--mcp-config', mcpConfig], {
|
|
47
|
+
const child = spawn('claude', ['--mcp-config', task.mcpConfig], {
|
|
52
48
|
cwd: '/tmp',
|
|
53
49
|
stdio: 'inherit',
|
|
54
50
|
});
|
package/src/commands/edit.ts
CHANGED
|
@@ -2,16 +2,13 @@ import {
|
|
|
2
2
|
readTaskConfig,
|
|
3
3
|
writeTaskConfig,
|
|
4
4
|
readGlobalConfig,
|
|
5
|
-
mcpConfigPath,
|
|
6
5
|
scriptPath,
|
|
7
6
|
plistPath,
|
|
8
7
|
} from '../config';
|
|
9
8
|
import { parseSchedule } from '../schedule';
|
|
10
|
-
import { validateMcpPresets } from '../mcp';
|
|
11
9
|
import {
|
|
12
10
|
generateScriptContent,
|
|
13
11
|
generatePlistContent,
|
|
14
|
-
generateMcpConfigContent,
|
|
15
12
|
} from '../generator';
|
|
16
13
|
import { bootout, bootstrap } from '../launchd';
|
|
17
14
|
|
|
@@ -22,10 +19,7 @@ export async function editCommand(args: string[]): Promise<void> {
|
|
|
22
19
|
options: {
|
|
23
20
|
schedule: { type: 'string' },
|
|
24
21
|
prompt: { type: 'string' },
|
|
25
|
-
'
|
|
26
|
-
mcp: { type: 'string' },
|
|
27
|
-
'allowed-tools': { type: 'string' },
|
|
28
|
-
'max-turns': { type: 'string' },
|
|
22
|
+
'mcp-config': { type: 'string' },
|
|
29
23
|
help: { type: 'boolean', short: 'h' },
|
|
30
24
|
},
|
|
31
25
|
allowPositionals: true,
|
|
@@ -59,38 +53,16 @@ export async function editCommand(args: string[]): Promise<void> {
|
|
|
59
53
|
|
|
60
54
|
if (values.prompt !== undefined) {
|
|
61
55
|
task.prompt = values.prompt;
|
|
62
|
-
task.promptFile = null;
|
|
63
56
|
changed = true;
|
|
64
57
|
}
|
|
65
58
|
|
|
66
|
-
if (values['
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.error(`Prompt file not found: ${task.promptFile}`);
|
|
59
|
+
if (values['mcp-config'] !== undefined) {
|
|
60
|
+
const mcpPath = values['mcp-config'];
|
|
61
|
+
if (mcpPath && !(await Bun.file(mcpPath).exists())) {
|
|
62
|
+
console.error(`MCP config file not found: ${mcpPath}`);
|
|
71
63
|
process.exit(1);
|
|
72
64
|
}
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (values.mcp !== undefined) {
|
|
77
|
-
const mcpNames = values.mcp.split(',').filter(Boolean);
|
|
78
|
-
const mcpError = validateMcpPresets(mcpNames);
|
|
79
|
-
if (mcpError) {
|
|
80
|
-
console.error(`Error: ${mcpError}`);
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
task.mcp = mcpNames;
|
|
84
|
-
changed = true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (values['allowed-tools'] !== undefined) {
|
|
88
|
-
task.allowedTools = values['allowed-tools'].split(',').filter(Boolean);
|
|
89
|
-
changed = true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (values['max-turns'] !== undefined) {
|
|
93
|
-
task.maxTurns = Number(values['max-turns']);
|
|
65
|
+
task.mcpConfig = mcpPath || null;
|
|
94
66
|
changed = true;
|
|
95
67
|
}
|
|
96
68
|
|
|
@@ -108,15 +80,6 @@ export async function editCommand(args: string[]): Promise<void> {
|
|
|
108
80
|
await writeTaskConfig(task);
|
|
109
81
|
console.log(`✓ Task config updated`);
|
|
110
82
|
|
|
111
|
-
// Regenerate MCP config
|
|
112
|
-
if (task.mcp.length > 0) {
|
|
113
|
-
await Bun.write(
|
|
114
|
-
mcpConfigPath(task.name),
|
|
115
|
-
generateMcpConfigContent(task.mcp),
|
|
116
|
-
);
|
|
117
|
-
console.log(`✓ MCP config regenerated`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
83
|
// Regenerate script
|
|
121
84
|
const scriptContent = generateScriptContent(task, globalConfig);
|
|
122
85
|
const scriptFile = scriptPath(task.name);
|
|
@@ -145,14 +108,11 @@ Usage: ccron edit <name> [options]
|
|
|
145
108
|
|
|
146
109
|
Options:
|
|
147
110
|
--schedule <cron> Update cron expression
|
|
148
|
-
--prompt <text> Update prompt
|
|
149
|
-
--
|
|
150
|
-
--mcp <names> Update MCP presets (comma-separated)
|
|
151
|
-
--allowed-tools <tools> Update allowed tools (comma-separated)
|
|
152
|
-
--max-turns <n> Update max turns
|
|
111
|
+
--prompt <text> Update prompt
|
|
112
|
+
--mcp-config <path> Update MCP config file path (pass "" to remove)
|
|
153
113
|
|
|
154
114
|
Examples:
|
|
155
115
|
ccron edit daily-summary --schedule "0 18 * * 1-5"
|
|
156
|
-
ccron edit daily-summary --prompt
|
|
157
|
-
ccron edit daily-summary --mcp slack
|
|
116
|
+
ccron edit daily-summary --prompt "新しいプロンプト"
|
|
117
|
+
ccron edit daily-summary --mcp-config ~/.config/ccron/mcp/slack.json`);
|
|
158
118
|
}
|
package/src/commands/remove.ts
CHANGED
|
@@ -3,9 +3,7 @@ import {
|
|
|
3
3
|
deleteTaskConfig,
|
|
4
4
|
scriptPath,
|
|
5
5
|
plistPath,
|
|
6
|
-
mcpConfigPath,
|
|
7
6
|
logPath,
|
|
8
|
-
listTaskConfigs,
|
|
9
7
|
} from '../config';
|
|
10
8
|
import { bootout } from '../launchd';
|
|
11
9
|
|
|
@@ -13,27 +11,21 @@ export async function removeCommand(args: string[]): Promise<void> {
|
|
|
13
11
|
if (args.includes('--help') || args.includes('-h')) {
|
|
14
12
|
console.log(`ccron remove - Remove a registered task
|
|
15
13
|
|
|
16
|
-
Usage: ccron remove <name>
|
|
14
|
+
Usage: ccron remove <name>
|
|
17
15
|
|
|
18
16
|
Removes a task by:
|
|
19
17
|
1. Unregistering from launchd (launchctl bootout)
|
|
20
|
-
2. Deleting plist, script, task config
|
|
21
|
-
3. Deleting MCP config (if not used by other tasks)
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
--purge Also delete log files (logs are kept by default)
|
|
18
|
+
2. Deleting plist, script, task config, log files
|
|
25
19
|
|
|
26
20
|
Examples:
|
|
27
|
-
ccron remove daily-summary
|
|
28
|
-
ccron remove daily-summary --purge`);
|
|
21
|
+
ccron remove daily-summary`);
|
|
29
22
|
return;
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
const purge = args.includes('--purge');
|
|
33
25
|
const name = args.find(a => !a.startsWith('-'));
|
|
34
26
|
|
|
35
27
|
if (!name) {
|
|
36
|
-
console.error('Usage: ccron remove <name>
|
|
28
|
+
console.error('Usage: ccron remove <name>');
|
|
37
29
|
process.exit(1);
|
|
38
30
|
}
|
|
39
31
|
|
|
@@ -55,31 +47,13 @@ Examples:
|
|
|
55
47
|
await tryUnlink(scriptPath(name));
|
|
56
48
|
console.log(`✓ Script removed`);
|
|
57
49
|
|
|
58
|
-
// 4. Delete
|
|
59
|
-
if (task.mcp.length > 0) {
|
|
60
|
-
const otherTasks = await listTaskConfigs();
|
|
61
|
-
const otherUsing = otherTasks.some(
|
|
62
|
-
t => t.name !== name && t.mcp.some(m => task.mcp.includes(m)),
|
|
63
|
-
);
|
|
64
|
-
if (!otherUsing) {
|
|
65
|
-
await tryUnlink(mcpConfigPath(name));
|
|
66
|
-
console.log(`✓ MCP config removed`);
|
|
67
|
-
} else {
|
|
68
|
-
console.log(` MCP config kept (used by other tasks)`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 5. Delete task config
|
|
50
|
+
// 4. Delete task config
|
|
73
51
|
await deleteTaskConfig(name);
|
|
74
52
|
console.log(`✓ Task config removed`);
|
|
75
53
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
console.log(`✓ Logs purged`);
|
|
80
|
-
} else {
|
|
81
|
-
console.log(` Logs kept at ${logPath(name)} (use --purge to remove)`);
|
|
82
|
-
}
|
|
54
|
+
// 5. Delete logs
|
|
55
|
+
await tryUnlink(logPath(name));
|
|
56
|
+
console.log(`✓ Logs removed`);
|
|
83
57
|
|
|
84
58
|
console.log(`\n"${name}" removed.`);
|
|
85
59
|
}
|
package/src/commands/show.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
plistPath,
|
|
5
5
|
logPath,
|
|
6
6
|
taskConfigPath,
|
|
7
|
-
mcpConfigPath,
|
|
8
7
|
} from '../config';
|
|
9
8
|
import { listOne } from '../launchd';
|
|
10
9
|
|
|
@@ -41,9 +40,8 @@ Displays task configuration, launchd status, and file locations.`);
|
|
|
41
40
|
? String(status.lastExitStatus)
|
|
42
41
|
: '-';
|
|
43
42
|
|
|
44
|
-
const promptDisplay =
|
|
45
|
-
|
|
46
|
-
: task.prompt && task.prompt.length > 80
|
|
43
|
+
const promptDisplay =
|
|
44
|
+
task.prompt && task.prompt.length > 80
|
|
47
45
|
? task.prompt.slice(0, 80) + '...'
|
|
48
46
|
: (task.prompt ?? '(none)');
|
|
49
47
|
|
|
@@ -51,9 +49,7 @@ Displays task configuration, launchd status, and file locations.`);
|
|
|
51
49
|
`Name: ${task.name}`,
|
|
52
50
|
`Schedule: ${task.schedule}`,
|
|
53
51
|
`Prompt: ${promptDisplay}`,
|
|
54
|
-
`MCP:
|
|
55
|
-
`Tools: ${task.allowedTools.length > 0 ? task.allowedTools.join(', ') : '(none)'}`,
|
|
56
|
-
`Max turns: ${task.maxTurns ?? '(default)'}`,
|
|
52
|
+
`MCP config: ${task.mcpConfig ?? '(none)'}`,
|
|
57
53
|
`Status: ${statusStr}`,
|
|
58
54
|
`Last exit: ${exitStr}`,
|
|
59
55
|
`Created: ${formatDate(task.createdAt)}`,
|
|
@@ -66,10 +62,6 @@ Displays task configuration, launchd status, and file locations.`);
|
|
|
66
62
|
` Log: ${shorten(logPath(name))}`,
|
|
67
63
|
];
|
|
68
64
|
|
|
69
|
-
if (task.mcp.length > 0) {
|
|
70
|
-
lines.push(` MCP: ${shorten(mcpConfigPath(name))}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
65
|
console.log(lines.join('\n'));
|
|
74
66
|
}
|
|
75
67
|
|
package/src/commands/test.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
readGlobalConfig,
|
|
4
4
|
scriptPath,
|
|
5
5
|
plistPath,
|
|
6
|
-
mcpConfigPath,
|
|
7
6
|
} from '../config';
|
|
8
7
|
import { listOne } from '../launchd';
|
|
9
8
|
import { homedir } from 'os';
|
|
@@ -29,6 +28,7 @@ Checks:
|
|
|
29
28
|
5. Script path is outside TCC-protected directories
|
|
30
29
|
6. Plist registered in launchd
|
|
31
30
|
7. MCP config exists (if task uses MCP)
|
|
31
|
+
8. MCP auth (if task uses MCP) — runs claude CLI to verify OAuth tokens
|
|
32
32
|
|
|
33
33
|
Each failed check shows a suggested fix command.
|
|
34
34
|
|
|
@@ -60,8 +60,9 @@ Example:
|
|
|
60
60
|
checks.push(checkScriptTccSafe(name));
|
|
61
61
|
checks.push(await checkPlistRegistered(name));
|
|
62
62
|
|
|
63
|
-
if (task.
|
|
64
|
-
checks.push(await checkMcpConfig(
|
|
63
|
+
if (task.mcpConfig) {
|
|
64
|
+
checks.push(await checkMcpConfig(task.mcpConfig));
|
|
65
|
+
checks.push(await checkMcpAuth(name, task.mcpConfig));
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// Print results
|
|
@@ -211,13 +212,58 @@ async function checkPlistRegistered(name: string): Promise<CheckResult> {
|
|
|
211
212
|
};
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
async function checkMcpConfig(
|
|
215
|
-
const
|
|
216
|
-
const exists = await Bun.file(path).exists();
|
|
215
|
+
async function checkMcpConfig(mcpConfigPath: string): Promise<CheckResult> {
|
|
216
|
+
const exists = await Bun.file(mcpConfigPath).exists();
|
|
217
217
|
return {
|
|
218
218
|
label: 'MCP config',
|
|
219
219
|
ok: exists,
|
|
220
|
-
detail: exists ? `exists (${
|
|
221
|
-
fix: exists ? undefined : `
|
|
220
|
+
detail: exists ? `exists (${mcpConfigPath})` : `not found (${mcpConfigPath})`,
|
|
221
|
+
fix: exists ? undefined : `Check the --mcp-config path`,
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
|
+
|
|
225
|
+
async function checkMcpAuth(name: string, mcpConfigPath: string): Promise<CheckResult> {
|
|
226
|
+
const exists = await Bun.file(mcpConfigPath).exists();
|
|
227
|
+
if (!exists) {
|
|
228
|
+
return {
|
|
229
|
+
label: 'MCP auth',
|
|
230
|
+
ok: false,
|
|
231
|
+
detail: 'skipped (MCP config file missing)',
|
|
232
|
+
fix: `Check the --mcp-config path`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const result =
|
|
238
|
+
await Bun.$`claude -p 'test' --mcp-config ${mcpConfigPath} --max-turns 1`
|
|
239
|
+
.quiet()
|
|
240
|
+
.timeout(30_000);
|
|
241
|
+
if (result.exitCode === 0) {
|
|
242
|
+
return {
|
|
243
|
+
label: 'MCP auth',
|
|
244
|
+
ok: true,
|
|
245
|
+
detail: 'authenticated',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const output = (
|
|
249
|
+
result.stdout.toString() + result.stderr.toString()
|
|
250
|
+
).trim();
|
|
251
|
+
return {
|
|
252
|
+
label: 'MCP auth',
|
|
253
|
+
ok: false,
|
|
254
|
+
detail: output || 'authentication failed',
|
|
255
|
+
fix: `Run: ccron auth ${name}`,
|
|
256
|
+
};
|
|
257
|
+
} catch (e) {
|
|
258
|
+
const message =
|
|
259
|
+
e instanceof Error && e.message.includes('timed out')
|
|
260
|
+
? 'timed out (30s)'
|
|
261
|
+
: 'failed to check';
|
|
262
|
+
return {
|
|
263
|
+
label: 'MCP auth',
|
|
264
|
+
ok: false,
|
|
265
|
+
detail: message,
|
|
266
|
+
fix: `Run: ccron auth ${name}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -7,10 +7,7 @@ export type TaskConfig = {
|
|
|
7
7
|
name: string;
|
|
8
8
|
schedule: string;
|
|
9
9
|
prompt: string | null;
|
|
10
|
-
|
|
11
|
-
mcp: string[];
|
|
12
|
-
allowedTools: string[];
|
|
13
|
-
maxTurns: number | null;
|
|
10
|
+
mcpConfig: string | null;
|
|
14
11
|
createdAt: string;
|
|
15
12
|
updatedAt: string;
|
|
16
13
|
};
|
|
@@ -28,7 +25,6 @@ const home = homedir();
|
|
|
28
25
|
export const PATHS = {
|
|
29
26
|
configDir: join(home, '.config', 'ccron'),
|
|
30
27
|
tasksDir: join(home, '.config', 'ccron', 'tasks'),
|
|
31
|
-
mcpDir: join(home, '.config', 'ccron', 'mcp'),
|
|
32
28
|
globalConfig: join(home, '.config', 'ccron', 'config.json'),
|
|
33
29
|
binDir: join(home, '.local', 'bin'),
|
|
34
30
|
logsDir: join(home, '.local', 'share', 'ccron', 'logs'),
|
|
@@ -39,10 +35,6 @@ export function taskConfigPath(name: string): string {
|
|
|
39
35
|
return join(PATHS.tasksDir, `${name}.json`);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
export function mcpConfigPath(name: string): string {
|
|
43
|
-
return join(PATHS.mcpDir, `${name}.json`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
38
|
export function scriptPath(name: string): string {
|
|
47
39
|
return join(PATHS.binDir, `ccron-${name}.sh`);
|
|
48
40
|
}
|
|
@@ -65,7 +57,6 @@ export async function ensureDirectories(): Promise<void> {
|
|
|
65
57
|
const { mkdir } = await import('fs/promises');
|
|
66
58
|
await Promise.all([
|
|
67
59
|
mkdir(PATHS.tasksDir, { recursive: true }),
|
|
68
|
-
mkdir(PATHS.mcpDir, { recursive: true }),
|
|
69
60
|
mkdir(PATHS.binDir, { recursive: true }),
|
|
70
61
|
mkdir(PATHS.logsDir, { recursive: true }),
|
|
71
62
|
mkdir(PATHS.launchAgentsDir, { recursive: true }),
|
package/src/generator.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { TaskConfig, GlobalConfig } from './config';
|
|
2
2
|
import type { CalendarInterval } from './schedule';
|
|
3
|
-
import { logPath,
|
|
4
|
-
import { buildMcpConfig } from './mcp';
|
|
3
|
+
import { logPath, plistLabel, scriptPath } from './config';
|
|
5
4
|
import { homedir } from 'os';
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -37,9 +36,7 @@ export function generateScriptContent(
|
|
|
37
36
|
];
|
|
38
37
|
|
|
39
38
|
// Prompt handling
|
|
40
|
-
if (task.
|
|
41
|
-
lines.push(`PROMPT=$(cat "${task.promptFile}")`);
|
|
42
|
-
} else if (task.prompt) {
|
|
39
|
+
if (task.prompt) {
|
|
43
40
|
// Escape single quotes for bash
|
|
44
41
|
const escaped = task.prompt.replace(/'/g, "'\\''");
|
|
45
42
|
lines.push(`PROMPT='${escaped}'`);
|
|
@@ -50,16 +47,8 @@ export function generateScriptContent(
|
|
|
50
47
|
// Build claude command
|
|
51
48
|
const claudeArgs: string[] = ['claude -p "$PROMPT"'];
|
|
52
49
|
|
|
53
|
-
if (task.
|
|
54
|
-
claudeArgs.push(` --mcp-config "${
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (task.allowedTools.length > 0) {
|
|
58
|
-
claudeArgs.push(` --allowedTools "${task.allowedTools.join(',')}"`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (task.maxTurns !== null) {
|
|
62
|
-
claudeArgs.push(` --max-turns ${task.maxTurns}`);
|
|
50
|
+
if (task.mcpConfig) {
|
|
51
|
+
claudeArgs.push(` --mcp-config "${task.mcpConfig}"`);
|
|
63
52
|
}
|
|
64
53
|
|
|
65
54
|
lines.push(claudeArgs.join(' \\\n'));
|
|
@@ -161,10 +150,3 @@ function escapeXml(str: string): string {
|
|
|
161
150
|
.replace(/'/g, ''');
|
|
162
151
|
}
|
|
163
152
|
|
|
164
|
-
/**
|
|
165
|
-
* Generate MCP config JSON string from task's MCP presets.
|
|
166
|
-
*/
|
|
167
|
-
export function generateMcpConfigContent(presets: string[]): string {
|
|
168
|
-
const config = buildMcpConfig(presets);
|
|
169
|
-
return JSON.stringify(config, null, 2);
|
|
170
|
-
}
|
package/src/mcp.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export type McpServerEntry = {
|
|
2
|
-
type: string;
|
|
3
|
-
url: string;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
const MCP_PRESETS: Record<string, McpServerEntry> = {
|
|
7
|
-
slack: {
|
|
8
|
-
type: 'http',
|
|
9
|
-
url: 'https://mcp.slack.com/mcp',
|
|
10
|
-
},
|
|
11
|
-
linear: {
|
|
12
|
-
type: 'http',
|
|
13
|
-
url: 'https://mcp.linear.app/mcp',
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function getPresetNames(): string[] {
|
|
18
|
-
return Object.keys(MCP_PRESETS);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isKnownPreset(name: string): boolean {
|
|
22
|
-
return name in MCP_PRESETS;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function validateMcpPresets(names: string[]): string | null {
|
|
26
|
-
const unknown = names.filter(n => !isKnownPreset(n));
|
|
27
|
-
if (unknown.length > 0) {
|
|
28
|
-
return `Unknown MCP preset(s): ${unknown.join(', ')}. Available: ${getPresetNames().join(', ')}`;
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Build an MCP config JSON object from preset names.
|
|
35
|
-
* Output format matches claude CLI's --mcp-config expectation.
|
|
36
|
-
*/
|
|
37
|
-
export function buildMcpConfig(presets: string[]): {
|
|
38
|
-
mcpServers: Record<string, McpServerEntry>;
|
|
39
|
-
} {
|
|
40
|
-
const servers: Record<string, McpServerEntry> = {};
|
|
41
|
-
for (const name of presets) {
|
|
42
|
-
const preset = MCP_PRESETS[name];
|
|
43
|
-
if (!preset) {
|
|
44
|
-
throw new Error(`Unknown MCP preset: ${name}`);
|
|
45
|
-
}
|
|
46
|
-
servers[name] = preset;
|
|
47
|
-
}
|
|
48
|
-
return { mcpServers: servers };
|
|
49
|
-
}
|