@mrsekut/ccron 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -6
- package/package.json +1 -1
- package/skills/ccron/SKILL.md +43 -25
- package/src/cli.ts +3 -5
- package/src/commands/add.ts +35 -57
- package/src/commands/auth.ts +8 -12
- package/src/commands/edit.ts +27 -46
- package/src/commands/remove.ts +8 -34
- package/src/commands/show.ts +4 -11
- package/src/commands/test.ts +54 -8
- package/src/config.ts +7 -10
- package/src/generator.ts +6 -24
- package/src/mcp.ts +0 -49
package/README.md
CHANGED
|
@@ -29,8 +29,9 @@ 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
|
+
--cwd ~/src/myproject
|
|
34
35
|
|
|
35
36
|
# Verify setup
|
|
36
37
|
ccron test daily-summary
|
|
@@ -51,7 +52,7 @@ ccron list
|
|
|
51
52
|
| `ccron log <name>` | Show logs (`--follow` for tail) |
|
|
52
53
|
| `ccron edit <name>` | Edit config, regenerate and reload |
|
|
53
54
|
| `ccron auth <name>` | Re-authenticate MCP servers |
|
|
54
|
-
| `ccron remove <name>` | Remove task
|
|
55
|
+
| `ccron remove <name>` | Remove task and logs |
|
|
55
56
|
|
|
56
57
|
## Schedule Format
|
|
57
58
|
|
|
@@ -66,10 +67,35 @@ Standard cron expression: `"minute hour * * day-of-week"`
|
|
|
66
67
|
|
|
67
68
|
Step values (`*/5`) and minute/hour ranges are not supported (launchd limitation).
|
|
68
69
|
|
|
69
|
-
##
|
|
70
|
+
## Working Directory
|
|
70
71
|
|
|
71
|
-
`
|
|
72
|
+
Tasks run in `/tmp` by default. Pass `--cwd` to run in a project directory instead:
|
|
72
73
|
|
|
73
74
|
```bash
|
|
74
|
-
ccron add --name my-task --schedule "0 9 * * *" --prompt "hello" --
|
|
75
|
+
ccron add --name my-task --schedule "0 9 * * *" --prompt "hello" --cwd ~/src/myproject
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This matters for more than file access. `claude` picks up its project context from the
|
|
79
|
+
directory it starts in, so with `--cwd` the task can also use that project's
|
|
80
|
+
`CLAUDE.md`, `.claude/skills/` and `.mcp.json`.
|
|
81
|
+
|
|
82
|
+
That makes it possible to keep the prompt itself in the project as a skill, and have the
|
|
83
|
+
task be a one-liner that invokes it:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
ccron add --name member-watch --schedule "0 17 * * 1-5" \
|
|
87
|
+
--prompt "/member-watch を実行して" \
|
|
88
|
+
--cwd ~/src/myproject
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The prompt then lives under version control, and manual runs and scheduled runs share the
|
|
92
|
+
same definition. Without `--cwd`, a task cannot read files outside `/tmp` — it will fail
|
|
93
|
+
with a permission error rather than a missing-file error.
|
|
94
|
+
|
|
95
|
+
## MCP
|
|
96
|
+
|
|
97
|
+
Pass your own MCP config JSON file via `--mcp-config`:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ccron add --name my-task --schedule "0 9 * * *" --prompt "hello" --mcp-config ~/mcp.json
|
|
75
101
|
```
|
package/package.json
CHANGED
package/skills/ccron/SKILL.md
CHANGED
|
@@ -16,38 +16,53 @@ 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
|
-
--
|
|
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
|
|
46
|
+
--cwd <path> Directory to run claude in (default: /tmp)
|
|
49
47
|
```
|
|
50
48
|
|
|
49
|
+
## Working Directory
|
|
50
|
+
|
|
51
|
+
**If the task touches files outside `/tmp`, pass `--cwd`.** Without it the run fails with a
|
|
52
|
+
permission error, not a missing-file error — an absolute path in the prompt does not help.
|
|
53
|
+
|
|
54
|
+
`claude` also reads its project context from the directory it starts in, so `--cwd` makes
|
|
55
|
+
that project's `CLAUDE.md`, `.claude/skills/` and `.mcp.json` available to the task.
|
|
56
|
+
|
|
57
|
+
Prefer putting the prompt in the project as a skill and keeping the task a one-liner:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ccron add --name member-watch --schedule "0 17 * * 1-5" \
|
|
61
|
+
--prompt "/member-watch を実行して" --cwd ~/src/myproject
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The prompt stays under version control, and manual and scheduled runs share one definition.
|
|
65
|
+
|
|
51
66
|
## Schedule (cron)
|
|
52
67
|
|
|
53
68
|
Format: `"minute hour * * day-of-week"` (5 fields)
|
|
@@ -66,18 +81,21 @@ Format: `"minute hour * * day-of-week"` (5 fields)
|
|
|
66
81
|
User: "Post a daily summary to Slack at 5pm on weekdays"
|
|
67
82
|
|
|
68
83
|
```bash
|
|
69
|
-
# 1.
|
|
70
|
-
cat >
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
# 1. Write MCP config
|
|
85
|
+
cat > ~/.config/ccron/mcp-slack.json << 'EOF'
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"slack": { "type": "http", "url": "https://mcp.slack.com/mcp" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
73
91
|
EOF
|
|
74
92
|
|
|
75
93
|
# 2. Register
|
|
76
94
|
ccron add \
|
|
77
95
|
--name daily-summary \
|
|
78
96
|
--schedule "0 17 * * 1-5" \
|
|
79
|
-
--prompt
|
|
80
|
-
--mcp slack
|
|
97
|
+
--prompt "Create a daily summary and post it to #daily-summary channel. Include: completed tasks, tomorrow's plan, blockers." \
|
|
98
|
+
--mcp-config ~/.config/ccron/mcp-slack.json
|
|
81
99
|
|
|
82
100
|
# 3. Verify
|
|
83
101
|
ccron test daily-summary
|
|
@@ -87,5 +105,5 @@ ccron test daily-summary
|
|
|
87
105
|
|
|
88
106
|
- `ccron test` fails: follow the suggested fix commands in the output
|
|
89
107
|
- MCP auth expired: `ccron auth <name>`
|
|
90
|
-
- Prompt change:
|
|
108
|
+
- Prompt change: `ccron edit <name> --prompt "<new prompt>"`
|
|
91
109
|
- Schedule change: `ccron edit <name> --schedule "<cron>"`
|
package/src/cli.ts
CHANGED
|
@@ -32,18 +32,16 @@ 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
|
|
44
|
+
ccron add --name member-watch --schedule "0 17 * * 1-5" --prompt "/member-watch を実行して" --cwd ~/src/myproject
|
|
47
45
|
ccron list
|
|
48
46
|
ccron show daily-summary
|
|
49
47
|
ccron test daily-summary
|
package/src/commands/add.ts
CHANGED
|
@@ -2,21 +2,19 @@ import {
|
|
|
2
2
|
type TaskConfig,
|
|
3
3
|
type GlobalConfig,
|
|
4
4
|
validateTaskName,
|
|
5
|
+
validateCwd,
|
|
5
6
|
readTaskConfig,
|
|
6
7
|
writeTaskConfig,
|
|
7
8
|
readGlobalConfig,
|
|
8
9
|
writeGlobalConfig,
|
|
9
10
|
ensureDirectories,
|
|
10
|
-
mcpConfigPath,
|
|
11
11
|
scriptPath,
|
|
12
12
|
plistPath,
|
|
13
13
|
} from '../config';
|
|
14
14
|
import { parseSchedule } from '../schedule';
|
|
15
|
-
import { validateMcpPresets } from '../mcp';
|
|
16
15
|
import {
|
|
17
16
|
generateScriptContent,
|
|
18
17
|
generatePlistContent,
|
|
19
|
-
generateMcpConfigContent,
|
|
20
18
|
} from '../generator';
|
|
21
19
|
import { bootstrap } from '../launchd';
|
|
22
20
|
|
|
@@ -24,10 +22,8 @@ type AddOptions = {
|
|
|
24
22
|
name: string;
|
|
25
23
|
schedule: string;
|
|
26
24
|
prompt: string | null;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
allowedTools: string[];
|
|
30
|
-
maxTurns: number | null;
|
|
25
|
+
mcpConfig: string | null;
|
|
26
|
+
cwd: string | null;
|
|
31
27
|
};
|
|
32
28
|
|
|
33
29
|
export async function addCommand(args: string[]): Promise<void> {
|
|
@@ -47,10 +43,8 @@ export async function addCommand(args: string[]): Promise<void> {
|
|
|
47
43
|
name: opts.name,
|
|
48
44
|
schedule: opts.schedule,
|
|
49
45
|
prompt: opts.prompt,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
allowedTools: opts.allowedTools,
|
|
53
|
-
maxTurns: opts.maxTurns,
|
|
46
|
+
mcpConfig: opts.mcpConfig,
|
|
47
|
+
cwd: opts.cwd,
|
|
54
48
|
createdAt: now,
|
|
55
49
|
updatedAt: now,
|
|
56
50
|
};
|
|
@@ -59,13 +53,6 @@ export async function addCommand(args: string[]): Promise<void> {
|
|
|
59
53
|
await writeTaskConfig(task);
|
|
60
54
|
console.log(`✓ Task config saved: ~/.config/ccron/tasks/${task.name}.json`);
|
|
61
55
|
|
|
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
56
|
// Generate script
|
|
70
57
|
const scriptContent = generateScriptContent(task, globalConfig);
|
|
71
58
|
const scriptFile = scriptPath(task.name);
|
|
@@ -99,10 +86,8 @@ function parseAddArgs(args: string[]): AddOptions {
|
|
|
99
86
|
name: { type: 'string' },
|
|
100
87
|
schedule: { type: 'string' },
|
|
101
88
|
prompt: { type: 'string' },
|
|
102
|
-
'
|
|
103
|
-
|
|
104
|
-
'allowed-tools': { type: 'string' },
|
|
105
|
-
'max-turns': { type: 'string' },
|
|
89
|
+
'mcp-config': { type: 'string' },
|
|
90
|
+
cwd: { type: 'string' },
|
|
106
91
|
help: { type: 'boolean', short: 'h' },
|
|
107
92
|
},
|
|
108
93
|
strict: true,
|
|
@@ -117,12 +102,8 @@ function parseAddArgs(args: string[]): AddOptions {
|
|
|
117
102
|
name: values.name ?? '',
|
|
118
103
|
schedule: values.schedule ?? '',
|
|
119
104
|
prompt: values.prompt ?? null,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
allowedTools: values['allowed-tools']
|
|
123
|
-
? values['allowed-tools'].split(',').filter(Boolean)
|
|
124
|
-
: [],
|
|
125
|
-
maxTurns: values['max-turns'] ? Number(values['max-turns']) : null,
|
|
105
|
+
mcpConfig: values['mcp-config'] ?? null,
|
|
106
|
+
cwd: values.cwd ?? null,
|
|
126
107
|
};
|
|
127
108
|
}
|
|
128
109
|
|
|
@@ -151,31 +132,28 @@ async function validateOptions(opts: AddOptions): Promise<void> {
|
|
|
151
132
|
}
|
|
152
133
|
|
|
153
134
|
// Prompt
|
|
154
|
-
if (!opts.prompt
|
|
155
|
-
errors.push('
|
|
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
|
-
}
|
|
135
|
+
if (!opts.prompt) {
|
|
136
|
+
errors.push('--prompt is required');
|
|
165
137
|
}
|
|
166
138
|
|
|
167
|
-
//
|
|
168
|
-
if (opts.
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
139
|
+
// Working directory
|
|
140
|
+
if (opts.cwd) {
|
|
141
|
+
const cwdError = validateCwd(opts.cwd);
|
|
142
|
+
if (cwdError) errors.push(cwdError);
|
|
143
|
+
else {
|
|
144
|
+
const { existsSync, statSync } = await import('fs');
|
|
145
|
+
if (!existsSync(opts.cwd) || !statSync(opts.cwd).isDirectory()) {
|
|
146
|
+
errors.push(`Working directory not found: ${opts.cwd}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
171
149
|
}
|
|
172
150
|
|
|
173
|
-
//
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
(!
|
|
177
|
-
|
|
178
|
-
|
|
151
|
+
// MCP config file
|
|
152
|
+
if (opts.mcpConfig) {
|
|
153
|
+
const file = Bun.file(opts.mcpConfig);
|
|
154
|
+
if (!(await file.exists())) {
|
|
155
|
+
errors.push(`MCP config file not found: ${opts.mcpConfig}`);
|
|
156
|
+
}
|
|
179
157
|
}
|
|
180
158
|
|
|
181
159
|
if (errors.length > 0) {
|
|
@@ -226,13 +204,14 @@ Usage: ccron add [options]
|
|
|
226
204
|
Required:
|
|
227
205
|
--name <name> Task name (lowercase letters, numbers, hyphens)
|
|
228
206
|
--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)
|
|
207
|
+
--prompt <text> Prompt string
|
|
231
208
|
|
|
232
209
|
Optional:
|
|
233
|
-
--mcp <
|
|
234
|
-
--
|
|
235
|
-
|
|
210
|
+
--mcp-config <path> Path to MCP config JSON file
|
|
211
|
+
--cwd <path> Directory to run claude in (default: /tmp)
|
|
212
|
+
Set this to a project directory so the task can read
|
|
213
|
+
its files and pick up its CLAUDE.md, .claude/skills
|
|
214
|
+
and .mcp.json
|
|
236
215
|
|
|
237
216
|
Schedule format (cron):
|
|
238
217
|
"minute hour * * day-of-week"
|
|
@@ -243,9 +222,8 @@ Schedule format (cron):
|
|
|
243
222
|
day-of-month and month: must be "*" (launchd limitation)
|
|
244
223
|
|
|
245
224
|
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"
|
|
225
|
+
ccron add --name daily-summary --schedule "15 17 * * 1-5" --prompt "日次サマリーを作成して"
|
|
226
|
+
ccron add --name weekly-review --schedule "0 22 * * 5" --prompt "週次レビューを作成して" --mcp-config ~/.config/ccron/mcp/slack.json
|
|
249
227
|
|
|
250
228
|
Note: Step values (*/5) and ranges in minute/hour are not supported by launchd.`);
|
|
251
229
|
}
|
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,14 @@ import {
|
|
|
2
2
|
readTaskConfig,
|
|
3
3
|
writeTaskConfig,
|
|
4
4
|
readGlobalConfig,
|
|
5
|
-
mcpConfigPath,
|
|
6
5
|
scriptPath,
|
|
7
6
|
plistPath,
|
|
7
|
+
validateCwd,
|
|
8
8
|
} from '../config';
|
|
9
9
|
import { parseSchedule } from '../schedule';
|
|
10
|
-
import { validateMcpPresets } from '../mcp';
|
|
11
10
|
import {
|
|
12
11
|
generateScriptContent,
|
|
13
12
|
generatePlistContent,
|
|
14
|
-
generateMcpConfigContent,
|
|
15
13
|
} from '../generator';
|
|
16
14
|
import { bootout, bootstrap } from '../launchd';
|
|
17
15
|
|
|
@@ -22,10 +20,8 @@ export async function editCommand(args: string[]): Promise<void> {
|
|
|
22
20
|
options: {
|
|
23
21
|
schedule: { type: 'string' },
|
|
24
22
|
prompt: { type: 'string' },
|
|
25
|
-
'
|
|
26
|
-
|
|
27
|
-
'allowed-tools': { type: 'string' },
|
|
28
|
-
'max-turns': { type: 'string' },
|
|
23
|
+
'mcp-config': { type: 'string' },
|
|
24
|
+
cwd: { type: 'string' },
|
|
29
25
|
help: { type: 'boolean', short: 'h' },
|
|
30
26
|
},
|
|
31
27
|
allowPositionals: true,
|
|
@@ -59,38 +55,34 @@ export async function editCommand(args: string[]): Promise<void> {
|
|
|
59
55
|
|
|
60
56
|
if (values.prompt !== undefined) {
|
|
61
57
|
task.prompt = values.prompt;
|
|
62
|
-
task.promptFile = null;
|
|
63
58
|
changed = true;
|
|
64
59
|
}
|
|
65
60
|
|
|
66
|
-
if (values['
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.error(`Prompt file not found: ${task.promptFile}`);
|
|
61
|
+
if (values['mcp-config'] !== undefined) {
|
|
62
|
+
const mcpPath = values['mcp-config'];
|
|
63
|
+
if (mcpPath && !(await Bun.file(mcpPath).exists())) {
|
|
64
|
+
console.error(`MCP config file not found: ${mcpPath}`);
|
|
71
65
|
process.exit(1);
|
|
72
66
|
}
|
|
67
|
+
task.mcpConfig = mcpPath || null;
|
|
73
68
|
changed = true;
|
|
74
69
|
}
|
|
75
70
|
|
|
76
|
-
if (values.
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
if (values.cwd !== undefined) {
|
|
72
|
+
const cwd = values.cwd;
|
|
73
|
+
if (cwd) {
|
|
74
|
+
const cwdError = validateCwd(cwd);
|
|
75
|
+
if (cwdError) {
|
|
76
|
+
console.error(`Error: ${cwdError}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const { existsSync, statSync } = await import('fs');
|
|
80
|
+
if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
|
|
81
|
+
console.error(`Working directory not found: ${cwd}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
82
84
|
}
|
|
83
|
-
task.
|
|
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']);
|
|
85
|
+
task.cwd = cwd || null;
|
|
94
86
|
changed = true;
|
|
95
87
|
}
|
|
96
88
|
|
|
@@ -108,15 +100,6 @@ export async function editCommand(args: string[]): Promise<void> {
|
|
|
108
100
|
await writeTaskConfig(task);
|
|
109
101
|
console.log(`✓ Task config updated`);
|
|
110
102
|
|
|
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
103
|
// Regenerate script
|
|
121
104
|
const scriptContent = generateScriptContent(task, globalConfig);
|
|
122
105
|
const scriptFile = scriptPath(task.name);
|
|
@@ -145,14 +128,12 @@ Usage: ccron edit <name> [options]
|
|
|
145
128
|
|
|
146
129
|
Options:
|
|
147
130
|
--schedule <cron> Update cron expression
|
|
148
|
-
--prompt <text> Update prompt
|
|
149
|
-
--
|
|
150
|
-
--
|
|
151
|
-
--allowed-tools <tools> Update allowed tools (comma-separated)
|
|
152
|
-
--max-turns <n> Update max turns
|
|
131
|
+
--prompt <text> Update prompt
|
|
132
|
+
--mcp-config <path> Update MCP config file path (pass "" to remove)
|
|
133
|
+
--cwd <path> Update the directory claude runs in (pass "" to reset to /tmp)
|
|
153
134
|
|
|
154
135
|
Examples:
|
|
155
136
|
ccron edit daily-summary --schedule "0 18 * * 1-5"
|
|
156
|
-
ccron edit daily-summary --prompt
|
|
157
|
-
ccron edit daily-summary --mcp slack
|
|
137
|
+
ccron edit daily-summary --prompt "新しいプロンプト"
|
|
138
|
+
ccron edit daily-summary --mcp-config ~/.config/ccron/mcp/slack.json`);
|
|
158
139
|
}
|
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,8 @@ 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
|
-
`
|
|
56
|
-
`Max turns: ${task.maxTurns ?? '(default)'}`,
|
|
52
|
+
`MCP config: ${task.mcpConfig ?? '(none)'}`,
|
|
53
|
+
`Working dir: ${task.cwd ?? '/tmp (default)'}`,
|
|
57
54
|
`Status: ${statusStr}`,
|
|
58
55
|
`Last exit: ${exitStr}`,
|
|
59
56
|
`Created: ${formatDate(task.createdAt)}`,
|
|
@@ -66,10 +63,6 @@ Displays task configuration, launchd status, and file locations.`);
|
|
|
66
63
|
` Log: ${shorten(logPath(name))}`,
|
|
67
64
|
];
|
|
68
65
|
|
|
69
|
-
if (task.mcp.length > 0) {
|
|
70
|
-
lines.push(` MCP: ${shorten(mcpConfigPath(name))}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
66
|
console.log(lines.join('\n'));
|
|
74
67
|
}
|
|
75
68
|
|
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,8 @@ export type TaskConfig = {
|
|
|
7
7
|
name: string;
|
|
8
8
|
schedule: string;
|
|
9
9
|
prompt: string | null;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
allowedTools: string[];
|
|
13
|
-
maxTurns: number | null;
|
|
10
|
+
mcpConfig: string | null;
|
|
11
|
+
cwd: string | null;
|
|
14
12
|
createdAt: string;
|
|
15
13
|
updatedAt: string;
|
|
16
14
|
};
|
|
@@ -28,7 +26,6 @@ const home = homedir();
|
|
|
28
26
|
export const PATHS = {
|
|
29
27
|
configDir: join(home, '.config', 'ccron'),
|
|
30
28
|
tasksDir: join(home, '.config', 'ccron', 'tasks'),
|
|
31
|
-
mcpDir: join(home, '.config', 'ccron', 'mcp'),
|
|
32
29
|
globalConfig: join(home, '.config', 'ccron', 'config.json'),
|
|
33
30
|
binDir: join(home, '.local', 'bin'),
|
|
34
31
|
logsDir: join(home, '.local', 'share', 'ccron', 'logs'),
|
|
@@ -39,10 +36,6 @@ export function taskConfigPath(name: string): string {
|
|
|
39
36
|
return join(PATHS.tasksDir, `${name}.json`);
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
export function mcpConfigPath(name: string): string {
|
|
43
|
-
return join(PATHS.mcpDir, `${name}.json`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
39
|
export function scriptPath(name: string): string {
|
|
47
40
|
return join(PATHS.binDir, `ccron-${name}.sh`);
|
|
48
41
|
}
|
|
@@ -65,7 +58,6 @@ export async function ensureDirectories(): Promise<void> {
|
|
|
65
58
|
const { mkdir } = await import('fs/promises');
|
|
66
59
|
await Promise.all([
|
|
67
60
|
mkdir(PATHS.tasksDir, { recursive: true }),
|
|
68
|
-
mkdir(PATHS.mcpDir, { recursive: true }),
|
|
69
61
|
mkdir(PATHS.binDir, { recursive: true }),
|
|
70
62
|
mkdir(PATHS.logsDir, { recursive: true }),
|
|
71
63
|
mkdir(PATHS.launchAgentsDir, { recursive: true }),
|
|
@@ -132,6 +124,11 @@ function defaultGlobalConfig(): GlobalConfig {
|
|
|
132
124
|
|
|
133
125
|
const TASK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
134
126
|
|
|
127
|
+
export function validateCwd(cwd: string): string | null {
|
|
128
|
+
if (!cwd.startsWith('/')) return 'Working directory must be an absolute path';
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
135
132
|
export function validateTaskName(name: string): string | null {
|
|
136
133
|
if (!name) return 'Task name is required';
|
|
137
134
|
if (!TASK_NAME_RE.test(name))
|
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
|
/**
|
|
@@ -32,14 +31,12 @@ export function generateScriptContent(
|
|
|
32
31
|
'',
|
|
33
32
|
`ulimit -n ${global.ulimit} 2>/dev/null || true`,
|
|
34
33
|
'',
|
|
35
|
-
|
|
34
|
+
`cd ${task.cwd ?? '/tmp'}`,
|
|
36
35
|
'',
|
|
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'));
|
|
@@ -113,7 +102,7 @@ export function generatePlistContent(
|
|
|
113
102
|
<string>${escapeXml(pathEntries)}</string>
|
|
114
103
|
</dict>
|
|
115
104
|
<key>WorkingDirectory</key>
|
|
116
|
-
<string
|
|
105
|
+
<string>${escapeXml(task.cwd ?? '/tmp')}</string>
|
|
117
106
|
<key>StartCalendarInterval</key>
|
|
118
107
|
${calendarIntervalXml}
|
|
119
108
|
<key>StandardOutPath</key>
|
|
@@ -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
|
-
}
|