@mrsekut/ccron 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/package.json +35 -0
- package/skills/ccron/SKILL.md +91 -0
- package/src/cli.ts +92 -0
- package/src/commands/add.ts +251 -0
- package/src/commands/auth.ts +67 -0
- package/src/commands/edit.ts +158 -0
- package/src/commands/list.ts +58 -0
- package/src/commands/log.ts +61 -0
- package/src/commands/remove.ts +94 -0
- package/src/commands/run.ts +61 -0
- package/src/commands/show.ts +91 -0
- package/src/commands/test.ts +223 -0
- package/src/config.ts +141 -0
- package/src/generator.ts +170 -0
- package/src/launchd.ts +61 -0
- package/src/mcp.ts +49 -0
- package/src/schedule.ts +121 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readTaskConfig,
|
|
3
|
+
writeTaskConfig,
|
|
4
|
+
readGlobalConfig,
|
|
5
|
+
mcpConfigPath,
|
|
6
|
+
scriptPath,
|
|
7
|
+
plistPath,
|
|
8
|
+
} from '../config';
|
|
9
|
+
import { parseSchedule } from '../schedule';
|
|
10
|
+
import { validateMcpPresets } from '../mcp';
|
|
11
|
+
import {
|
|
12
|
+
generateScriptContent,
|
|
13
|
+
generatePlistContent,
|
|
14
|
+
generateMcpConfigContent,
|
|
15
|
+
} from '../generator';
|
|
16
|
+
import { bootout, bootstrap } from '../launchd';
|
|
17
|
+
|
|
18
|
+
export async function editCommand(args: string[]): Promise<void> {
|
|
19
|
+
const { parseArgs } = require('util');
|
|
20
|
+
const { values, positionals } = parseArgs({
|
|
21
|
+
args,
|
|
22
|
+
options: {
|
|
23
|
+
schedule: { type: 'string' },
|
|
24
|
+
prompt: { type: 'string' },
|
|
25
|
+
'prompt-file': { type: 'string' },
|
|
26
|
+
mcp: { type: 'string' },
|
|
27
|
+
'allowed-tools': { type: 'string' },
|
|
28
|
+
'max-turns': { type: 'string' },
|
|
29
|
+
help: { type: 'boolean', short: 'h' },
|
|
30
|
+
},
|
|
31
|
+
allowPositionals: true,
|
|
32
|
+
strict: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (values.help) {
|
|
36
|
+
printEditHelp();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const name = positionals[0] as string | undefined;
|
|
41
|
+
if (!name) {
|
|
42
|
+
console.error('Usage: ccron edit <name> [options]');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const task = await readTaskConfig(name);
|
|
47
|
+
if (!task) {
|
|
48
|
+
console.error(`Task "${name}" not found.`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let changed = false;
|
|
53
|
+
|
|
54
|
+
if (values.schedule !== undefined) {
|
|
55
|
+
task.schedule = values.schedule;
|
|
56
|
+
parseSchedule(task.schedule);
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (values.prompt !== undefined) {
|
|
61
|
+
task.prompt = values.prompt;
|
|
62
|
+
task.promptFile = null;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
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}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
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']);
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!changed) {
|
|
98
|
+
console.log('No changes specified. Use --help to see available options.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
task.updatedAt = new Date().toISOString();
|
|
103
|
+
|
|
104
|
+
const globalConfig = await readGlobalConfig();
|
|
105
|
+
const intervals = parseSchedule(task.schedule);
|
|
106
|
+
|
|
107
|
+
// Update task config
|
|
108
|
+
await writeTaskConfig(task);
|
|
109
|
+
console.log(`✓ Task config updated`);
|
|
110
|
+
|
|
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
|
+
// Regenerate script
|
|
121
|
+
const scriptContent = generateScriptContent(task, globalConfig);
|
|
122
|
+
const scriptFile = scriptPath(task.name);
|
|
123
|
+
await Bun.write(scriptFile, scriptContent);
|
|
124
|
+
const { chmod } = await import('fs/promises');
|
|
125
|
+
await chmod(scriptFile, 0o755);
|
|
126
|
+
console.log(`✓ Script regenerated`);
|
|
127
|
+
|
|
128
|
+
// Regenerate plist
|
|
129
|
+
const plistContent = generatePlistContent(task, globalConfig, intervals);
|
|
130
|
+
await Bun.write(plistPath(task.name), plistContent);
|
|
131
|
+
console.log(`✓ Plist regenerated`);
|
|
132
|
+
|
|
133
|
+
// Reload launchd
|
|
134
|
+
await bootout(task.name);
|
|
135
|
+
await bootstrap(plistPath(task.name));
|
|
136
|
+
console.log(`✓ launchd reloaded`);
|
|
137
|
+
|
|
138
|
+
console.log(`\n"${task.name}" updated.`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printEditHelp() {
|
|
142
|
+
console.log(`ccron edit - Edit a task's configuration
|
|
143
|
+
|
|
144
|
+
Usage: ccron edit <name> [options]
|
|
145
|
+
|
|
146
|
+
Options:
|
|
147
|
+
--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
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
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`);
|
|
158
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { listTaskConfigs } from '../config';
|
|
2
|
+
import { listOne } from '../launchd';
|
|
3
|
+
|
|
4
|
+
export async function listCommand(args: string[]): Promise<void> {
|
|
5
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
6
|
+
console.log(`ccron list - List all registered tasks
|
|
7
|
+
|
|
8
|
+
Usage: ccron list
|
|
9
|
+
|
|
10
|
+
Shows all tasks with their schedule, launchd status, and last exit code.
|
|
11
|
+
|
|
12
|
+
Columns:
|
|
13
|
+
NAME Task name
|
|
14
|
+
SCHEDULE Cron expression
|
|
15
|
+
STATUS "active" (loaded in launchd) or "not loaded"
|
|
16
|
+
LAST EXIT Exit code of last execution (0 = success)`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tasks = await listTaskConfigs();
|
|
21
|
+
|
|
22
|
+
if (tasks.length === 0) {
|
|
23
|
+
console.log('No tasks registered. Use `ccron add` to create one.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Header
|
|
28
|
+
const nameW = Math.max(4, ...tasks.map(t => t.name.length));
|
|
29
|
+
const schedW = Math.max(8, ...tasks.map(t => t.schedule.length));
|
|
30
|
+
|
|
31
|
+
const header = [
|
|
32
|
+
'NAME'.padEnd(nameW),
|
|
33
|
+
'SCHEDULE'.padEnd(schedW),
|
|
34
|
+
'STATUS'.padEnd(10),
|
|
35
|
+
'LAST EXIT',
|
|
36
|
+
].join(' ');
|
|
37
|
+
|
|
38
|
+
console.log(header);
|
|
39
|
+
console.log('-'.repeat(header.length));
|
|
40
|
+
|
|
41
|
+
for (const task of tasks) {
|
|
42
|
+
const status = await listOne(task.name);
|
|
43
|
+
const statusStr = status ? 'active' : 'not loaded';
|
|
44
|
+
const exitStr =
|
|
45
|
+
status?.lastExitStatus !== null && status?.lastExitStatus !== undefined
|
|
46
|
+
? String(status.lastExitStatus)
|
|
47
|
+
: '-';
|
|
48
|
+
|
|
49
|
+
console.log(
|
|
50
|
+
[
|
|
51
|
+
task.name.padEnd(nameW),
|
|
52
|
+
task.schedule.padEnd(schedW),
|
|
53
|
+
statusStr.padEnd(10),
|
|
54
|
+
exitStr,
|
|
55
|
+
].join(' '),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readTaskConfig, logPath } from '../config';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
export async function logCommand(args: string[]): Promise<void> {
|
|
5
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
6
|
+
console.log(`ccron log - Show task execution logs
|
|
7
|
+
|
|
8
|
+
Usage: ccron log <name> [--follow]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--follow, -f Tail the log in real-time (Ctrl+C to stop)
|
|
12
|
+
|
|
13
|
+
Without --follow, shows the last 50 lines of the log.
|
|
14
|
+
|
|
15
|
+
Log location: ~/.local/share/ccron/logs/<name>.log
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
ccron log daily-summary
|
|
19
|
+
ccron log daily-summary --follow`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const name = args.find(a => !a.startsWith('-'));
|
|
24
|
+
const follow = args.includes('--follow') || args.includes('-f');
|
|
25
|
+
|
|
26
|
+
if (!name) {
|
|
27
|
+
console.error('Usage: ccron log <name> [--follow]');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const task = await readTaskConfig(name);
|
|
32
|
+
if (!task) {
|
|
33
|
+
console.error(`Task "${name}" not found.`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const log = logPath(name);
|
|
38
|
+
const file = Bun.file(log);
|
|
39
|
+
|
|
40
|
+
if (!(await file.exists())) {
|
|
41
|
+
console.log(`No logs yet for "${name}".`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (follow) {
|
|
46
|
+
const tail = spawn('tail', ['-f', log], {
|
|
47
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
48
|
+
});
|
|
49
|
+
process.on('SIGINT', () => {
|
|
50
|
+
tail.kill();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
});
|
|
53
|
+
await new Promise<void>(resolve => {
|
|
54
|
+
tail.on('close', () => resolve());
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
// Show last 50 lines
|
|
58
|
+
const result = await Bun.$`tail -50 ${log}`.quiet();
|
|
59
|
+
console.log(result.stdout.toString());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readTaskConfig,
|
|
3
|
+
deleteTaskConfig,
|
|
4
|
+
scriptPath,
|
|
5
|
+
plistPath,
|
|
6
|
+
mcpConfigPath,
|
|
7
|
+
logPath,
|
|
8
|
+
listTaskConfigs,
|
|
9
|
+
} from '../config';
|
|
10
|
+
import { bootout } from '../launchd';
|
|
11
|
+
|
|
12
|
+
export async function removeCommand(args: string[]): Promise<void> {
|
|
13
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
14
|
+
console.log(`ccron remove - Remove a registered task
|
|
15
|
+
|
|
16
|
+
Usage: ccron remove <name> [--purge]
|
|
17
|
+
|
|
18
|
+
Removes a task by:
|
|
19
|
+
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)
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
ccron remove daily-summary
|
|
28
|
+
ccron remove daily-summary --purge`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const purge = args.includes('--purge');
|
|
33
|
+
const name = args.find(a => !a.startsWith('-'));
|
|
34
|
+
|
|
35
|
+
if (!name) {
|
|
36
|
+
console.error('Usage: ccron remove <name> [--purge]');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const task = await readTaskConfig(name);
|
|
41
|
+
if (!task) {
|
|
42
|
+
console.error(`Task "${name}" not found.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. Unregister from launchd
|
|
47
|
+
await bootout(name);
|
|
48
|
+
console.log(`✓ Unregistered from launchd`);
|
|
49
|
+
|
|
50
|
+
// 2. Delete plist
|
|
51
|
+
await tryUnlink(plistPath(name));
|
|
52
|
+
console.log(`✓ Plist removed`);
|
|
53
|
+
|
|
54
|
+
// 3. Delete script
|
|
55
|
+
await tryUnlink(scriptPath(name));
|
|
56
|
+
console.log(`✓ Script removed`);
|
|
57
|
+
|
|
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
|
|
73
|
+
await deleteTaskConfig(name);
|
|
74
|
+
console.log(`✓ Task config removed`);
|
|
75
|
+
|
|
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
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\n"${name}" removed.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function tryUnlink(path: string): Promise<void> {
|
|
88
|
+
const { unlink } = await import('fs/promises');
|
|
89
|
+
try {
|
|
90
|
+
await unlink(path);
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readTaskConfig, logPath } from '../config';
|
|
2
|
+
import { kickstart } from '../launchd';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
|
|
5
|
+
export async function runCommand(args: string[]): Promise<void> {
|
|
6
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
7
|
+
console.log(`ccron run - Manually trigger a task
|
|
8
|
+
|
|
9
|
+
Usage: ccron run <name>
|
|
10
|
+
|
|
11
|
+
Kicks start the task via launchctl and tails the log file in real-time.
|
|
12
|
+
Press Ctrl+C to stop following the log (the task continues running).
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
ccron run daily-summary`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const name = args[0];
|
|
20
|
+
if (!name) {
|
|
21
|
+
console.error('Usage: ccron run <name>');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const task = await readTaskConfig(name);
|
|
26
|
+
if (!task) {
|
|
27
|
+
console.error(`Task "${name}" not found.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const log = logPath(name);
|
|
32
|
+
|
|
33
|
+
// Ensure log file exists for tail
|
|
34
|
+
await Bun.write(
|
|
35
|
+
log,
|
|
36
|
+
await Bun.file(log)
|
|
37
|
+
.text()
|
|
38
|
+
.catch(() => ''),
|
|
39
|
+
{
|
|
40
|
+
createPath: true,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
console.log(`Kicking start ${name}...`);
|
|
45
|
+
await kickstart(name);
|
|
46
|
+
console.log(`Started. Tailing log (Ctrl+C to stop):\n`);
|
|
47
|
+
|
|
48
|
+
// tail -f the log file
|
|
49
|
+
const tail = spawn('tail', ['-f', log], {
|
|
50
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
process.on('SIGINT', () => {
|
|
54
|
+
tail.kill();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await new Promise<void>(resolve => {
|
|
59
|
+
tail.on('close', () => resolve());
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readTaskConfig,
|
|
3
|
+
scriptPath,
|
|
4
|
+
plistPath,
|
|
5
|
+
logPath,
|
|
6
|
+
taskConfigPath,
|
|
7
|
+
mcpConfigPath,
|
|
8
|
+
} from '../config';
|
|
9
|
+
import { listOne } from '../launchd';
|
|
10
|
+
|
|
11
|
+
export async function showCommand(args: string[]): Promise<void> {
|
|
12
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
13
|
+
console.log(`ccron show - Show detailed information about a task
|
|
14
|
+
|
|
15
|
+
Usage: ccron show <name>
|
|
16
|
+
|
|
17
|
+
Displays task configuration, launchd status, and file locations.`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const name = args[0];
|
|
22
|
+
if (!name) {
|
|
23
|
+
console.error('Task name is required. Usage: ccron show <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
|
+
const status = await listOne(name);
|
|
34
|
+
const statusStr = status
|
|
35
|
+
? status.pid
|
|
36
|
+
? `active (pid: ${status.pid})`
|
|
37
|
+
: 'active (pid: idle)'
|
|
38
|
+
: 'not loaded';
|
|
39
|
+
const exitStr =
|
|
40
|
+
status?.lastExitStatus !== null && status?.lastExitStatus !== undefined
|
|
41
|
+
? String(status.lastExitStatus)
|
|
42
|
+
: '-';
|
|
43
|
+
|
|
44
|
+
const promptDisplay = task.promptFile
|
|
45
|
+
? `(file) ${task.promptFile}`
|
|
46
|
+
: task.prompt && task.prompt.length > 80
|
|
47
|
+
? task.prompt.slice(0, 80) + '...'
|
|
48
|
+
: (task.prompt ?? '(none)');
|
|
49
|
+
|
|
50
|
+
const lines = [
|
|
51
|
+
`Name: ${task.name}`,
|
|
52
|
+
`Schedule: ${task.schedule}`,
|
|
53
|
+
`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)'}`,
|
|
57
|
+
`Status: ${statusStr}`,
|
|
58
|
+
`Last exit: ${exitStr}`,
|
|
59
|
+
`Created: ${formatDate(task.createdAt)}`,
|
|
60
|
+
`Updated: ${formatDate(task.updatedAt)}`,
|
|
61
|
+
``,
|
|
62
|
+
`Files:`,
|
|
63
|
+
` Config: ${shorten(taskConfigPath(name))}`,
|
|
64
|
+
` Script: ${shorten(scriptPath(name))}`,
|
|
65
|
+
` Plist: ${shorten(plistPath(name))}`,
|
|
66
|
+
` Log: ${shorten(logPath(name))}`,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
if (task.mcp.length > 0) {
|
|
70
|
+
lines.push(` MCP: ${shorten(mcpConfigPath(name))}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(lines.join('\n'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatDate(iso: string): string {
|
|
77
|
+
const d = new Date(iso);
|
|
78
|
+
return d.toLocaleString('ja-JP', {
|
|
79
|
+
year: 'numeric',
|
|
80
|
+
month: '2-digit',
|
|
81
|
+
day: '2-digit',
|
|
82
|
+
hour: '2-digit',
|
|
83
|
+
minute: '2-digit',
|
|
84
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shorten(path: string): string {
|
|
89
|
+
const home = process.env['HOME'] ?? '';
|
|
90
|
+
return home && path.startsWith(home) ? '~' + path.slice(home.length) : path;
|
|
91
|
+
}
|