@mrsekut/ccron 0.1.0

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