@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 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-file ./prompts/daily-summary.txt \
33
- --mcp slack
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 (`--purge` for logs) |
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 Presets
69
+ ## MCP
70
70
 
71
- `slack`, `linear` are available as built-in presets.
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 slack,linear
74
+ ccron add --name my-task --schedule "0 9 * * *" --prompt "hello" --mcp-config ~/mcp.json
75
75
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrsekut/ccron",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Schedule claude -p execution on macOS with launchd",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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. Create a prompt file (`./prompts/<name>.txt` or similar, git-manageable location)
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 (`--purge` for logs too) |
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 (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
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. 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.
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-file ./prompts/daily-summary.txt \
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: just edit the file if using `--prompt-file` (no re-registration needed)
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-file ./prompts/daily.txt
46
- ccron add --name weekly-review --schedule "0 22 * * 5" --prompt "週次レビュー" --mcp slack
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
@@ -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
- promptFile: string | null;
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
- promptFile: opts.promptFile,
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
- 'prompt-file': { type: 'string' },
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
- 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,
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 && !opts.promptFile) {
155
- errors.push('Either --prompt or --prompt-file is required');
130
+ if (!opts.prompt) {
131
+ errors.push('--prompt is required');
156
132
  }
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);
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(`Prompt file not found: ${opts.promptFile}`);
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 (mutually exclusive with --prompt-file)
230
- --prompt-file <path> Path to prompt file (mutually exclusive with --prompt)
190
+ --prompt <text> Prompt string
231
191
 
232
192
  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
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-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"
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
  }
@@ -1,4 +1,4 @@
1
- import { readTaskConfig, mcpConfigPath } from '../config';
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 (Slack, Linear, etc.).
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.mcp.length === 0) {
34
- console.log(`Task "${name}" has no MCP servers configured.`);
33
+ if (!task.mcpConfig) {
34
+ console.log(`Task "${name}" has no MCP config.`);
35
35
  return;
36
36
  }
37
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
- );
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 servers: ${task.mcp.join(', ')}`);
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
  });
@@ -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
- 'prompt-file': { type: 'string' },
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['prompt-file'] !== undefined) {
67
- task.promptFile = values['prompt-file'];
68
- task.prompt = null;
69
- if (!(await Bun.file(task.promptFile!).exists())) {
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
- changed = true;
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 (clears --prompt-file)
149
- --prompt-file <path> Update prompt file (clears --prompt)
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-file ./prompts/v2.txt
157
- ccron edit daily-summary --mcp slack,linear`);
116
+ ccron edit daily-summary --prompt "新しいプロンプト"
117
+ ccron edit daily-summary --mcp-config ~/.config/ccron/mcp/slack.json`);
158
118
  }
@@ -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> [--purge]
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> [--purge]');
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 MCP config if not shared by other tasks
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
- // 6. Logs
77
- if (purge) {
78
- await tryUnlink(logPath(name));
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
  }
@@ -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 = task.promptFile
45
- ? `(file) ${task.promptFile}`
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: ${task.mcp.length > 0 ? task.mcp.join(', ') : '(none)'}`,
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
 
@@ -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.mcp.length > 0) {
64
- checks.push(await checkMcpConfig(name));
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(name: string): Promise<CheckResult> {
215
- const path = mcpConfigPath(name);
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 (${path})` : `not found (${path})`,
221
- fix: exists ? undefined : `Re-register with --mcp flag`,
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
- promptFile: string | null;
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, mcpConfigPath, plistLabel, scriptPath } from './config';
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.promptFile) {
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.mcp.length > 0) {
54
- claudeArgs.push(` --mcp-config "${mcpConfigPath(task.name)}"`);
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, '&apos;');
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
- }