@1zero/agentcron 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.
@@ -0,0 +1,11 @@
1
+ import type { Skill } from '../core/skill-parser.js';
2
+ import type { AgentAdapter, InstalledAutomation, WriteOptions } from './types.js';
3
+ export declare class ClaudeAdapter implements AgentAdapter {
4
+ readonly agentId: "claude";
5
+ readonly agentName = "Claude Code";
6
+ private get basePath();
7
+ write(skill: Skill, _options: WriteOptions): Promise<string>;
8
+ read(name: string): Promise<Skill>;
9
+ list(): Promise<InstalledAutomation[]>;
10
+ remove(name: string): Promise<void>;
11
+ }
@@ -0,0 +1,76 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import path from 'path';
4
+ import { parseSkillMd, serializeSkillMd } from '../core/skill-parser.js';
5
+ export class ClaudeAdapter {
6
+ agentId = 'claude';
7
+ agentName = 'Claude Code';
8
+ get basePath() {
9
+ return path.join(homedir(), '.claude', 'scheduled-tasks');
10
+ }
11
+ async write(skill, _options) {
12
+ const taskDir = path.join(this.basePath, skill.meta.name);
13
+ mkdirSync(taskDir, { recursive: true });
14
+ const claudeMeta = {
15
+ name: skill.meta.name,
16
+ description: skill.meta.description
17
+ };
18
+ const content = serializeSkillMd(claudeMeta, skill.prompt);
19
+ writeFileSync(path.join(taskDir, 'SKILL.md'), content, 'utf-8');
20
+ for (const [filename, fileContent] of skill.auxiliaryFiles) {
21
+ writeFileSync(path.join(taskDir, filename), fileContent, 'utf-8');
22
+ }
23
+ return taskDir;
24
+ }
25
+ async read(name) {
26
+ const taskDir = path.join(this.basePath, name);
27
+ const skillMdPath = path.join(taskDir, 'SKILL.md');
28
+ if (!existsSync(skillMdPath)) {
29
+ throw new Error('Claude scheduled task "' + name + '" not found');
30
+ }
31
+ const raw = readFileSync(skillMdPath, 'utf-8');
32
+ const { meta, prompt } = parseSkillMd(raw);
33
+ meta.source_agent = 'claude';
34
+ const auxiliaryFiles = new Map();
35
+ for (const file of readdirSync(taskDir)) {
36
+ if (file !== 'SKILL.md') {
37
+ const filePath = path.join(taskDir, file);
38
+ try {
39
+ auxiliaryFiles.set(file, readFileSync(filePath, 'utf-8'));
40
+ }
41
+ catch {
42
+ // 바이너리는 건너뜀
43
+ }
44
+ }
45
+ }
46
+ return { meta, prompt, auxiliaryFiles };
47
+ }
48
+ async list() {
49
+ if (!existsSync(this.basePath))
50
+ return [];
51
+ const entries = readdirSync(this.basePath, { withFileTypes: true });
52
+ const results = [];
53
+ for (const entry of entries) {
54
+ if (entry.isDirectory()) {
55
+ const skillPath = path.join(this.basePath, entry.name, 'SKILL.md');
56
+ if (existsSync(skillPath)) {
57
+ const raw = readFileSync(skillPath, 'utf-8');
58
+ const { meta } = parseSkillMd(raw);
59
+ results.push({
60
+ name: meta.name || entry.name,
61
+ agent: 'claude',
62
+ schedule: typeof meta.schedule === 'string' ? meta.schedule : undefined,
63
+ path: path.join(this.basePath, entry.name)
64
+ });
65
+ }
66
+ }
67
+ }
68
+ return results;
69
+ }
70
+ async remove(name) {
71
+ const taskDir = path.join(this.basePath, name);
72
+ if (existsSync(taskDir)) {
73
+ rmSync(taskDir, { recursive: true, force: true });
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,11 @@
1
+ import type { Skill } from '../core/skill-parser.js';
2
+ import type { AgentAdapter, InstalledAutomation, WriteOptions } from './types.js';
3
+ export declare class CodexAdapter implements AgentAdapter {
4
+ readonly agentId: "codex";
5
+ readonly agentName = "Codex App";
6
+ private get basePath();
7
+ write(skill: Skill, options: WriteOptions): Promise<string>;
8
+ read(id: string): Promise<Skill>;
9
+ list(): Promise<InstalledAutomation[]>;
10
+ remove(id: string): Promise<void>;
11
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import path from 'path';
4
+ import TOML from '@iarna/toml';
5
+ import { isCron } from '../core/schedule.js';
6
+ import { cronToRRule } from '../core/schedule.js';
7
+ export class CodexAdapter {
8
+ agentId = 'codex';
9
+ agentName = 'Codex App';
10
+ get basePath() {
11
+ return path.join(homedir(), '.codex', 'automations');
12
+ }
13
+ async write(skill, options) {
14
+ const automationDir = path.join(this.basePath, skill.meta.name);
15
+ mkdirSync(automationDir, { recursive: true });
16
+ let rrule = '';
17
+ if (skill.meta.schedule) {
18
+ const schedule = String(skill.meta.schedule);
19
+ rrule = isCron(schedule) ? cronToRRule(schedule) : schedule;
20
+ if (!rrule.startsWith('RRULE:'))
21
+ rrule = 'RRULE:' + rrule;
22
+ }
23
+ const now = Date.now();
24
+ const toml = {
25
+ version: 1,
26
+ id: skill.meta.name,
27
+ kind: 'cron',
28
+ name: skill.meta.description || skill.meta.name,
29
+ prompt: skill.prompt,
30
+ status: options.status || 'ACTIVE',
31
+ rrule,
32
+ model: typeof skill.meta.model === 'string' ? skill.meta.model : 'gpt-5.4',
33
+ reasoning_effort: typeof skill.meta.reasoning_effort === 'string' ? skill.meta.reasoning_effort : 'high',
34
+ execution_environment: 'local',
35
+ cwds: options.cwds || [process.cwd()],
36
+ created_at: now,
37
+ updated_at: now
38
+ };
39
+ writeFileSync(path.join(automationDir, 'automation.toml'), TOML.stringify(toml), 'utf-8');
40
+ for (const [filename, fileContent] of skill.auxiliaryFiles) {
41
+ writeFileSync(path.join(automationDir, filename), fileContent, 'utf-8');
42
+ }
43
+ return automationDir;
44
+ }
45
+ async read(id) {
46
+ const automationDir = path.join(this.basePath, id);
47
+ const tomlPath = path.join(automationDir, 'automation.toml');
48
+ if (!existsSync(tomlPath)) {
49
+ throw new Error('Codex automation "' + id + '" not found');
50
+ }
51
+ const raw = readFileSync(tomlPath, 'utf-8');
52
+ const toml = TOML.parse(raw);
53
+ const auxiliaryFiles = new Map();
54
+ const skipFiles = new Set(['automation.toml', 'memory.md', 'result.md', 'result.json', '.DS_Store']);
55
+ for (const file of readdirSync(automationDir)) {
56
+ if (!skipFiles.has(file) && !file.startsWith('result-') && !file.endsWith('.json')) {
57
+ const filePath = path.join(automationDir, file);
58
+ try {
59
+ auxiliaryFiles.set(file, readFileSync(filePath, 'utf-8'));
60
+ }
61
+ catch {
62
+ // 바이너리는 건너뜀
63
+ }
64
+ }
65
+ }
66
+ return {
67
+ meta: {
68
+ name: toml.id,
69
+ description: toml.name,
70
+ schedule: toml.rrule,
71
+ model: toml.model,
72
+ reasoning_effort: toml.reasoning_effort,
73
+ source_agent: 'codex'
74
+ },
75
+ prompt: toml.prompt,
76
+ auxiliaryFiles
77
+ };
78
+ }
79
+ async list() {
80
+ if (!existsSync(this.basePath))
81
+ return [];
82
+ const entries = readdirSync(this.basePath, { withFileTypes: true });
83
+ const results = [];
84
+ for (const entry of entries) {
85
+ if (entry.isDirectory()) {
86
+ const tomlPath = path.join(this.basePath, entry.name, 'automation.toml');
87
+ if (existsSync(tomlPath)) {
88
+ try {
89
+ const raw = readFileSync(tomlPath, 'utf-8');
90
+ const toml = TOML.parse(raw);
91
+ results.push({
92
+ name: toml.id || entry.name,
93
+ agent: 'codex',
94
+ schedule: toml.rrule,
95
+ status: toml.status,
96
+ path: path.join(this.basePath, entry.name)
97
+ });
98
+ }
99
+ catch {
100
+ // 손상된 파일은 건너뜀
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return results;
106
+ }
107
+ async remove(id) {
108
+ const automationDir = path.join(this.basePath, id);
109
+ if (existsSync(automationDir)) {
110
+ rmSync(automationDir, { recursive: true, force: true });
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,20 @@
1
+ import type { Skill } from '../core/skill-parser.js';
2
+ export interface InstalledAutomation {
3
+ name: string;
4
+ agent: 'claude' | 'codex';
5
+ schedule?: string;
6
+ status?: string;
7
+ path: string;
8
+ }
9
+ export interface AgentAdapter {
10
+ readonly agentId: 'claude' | 'codex';
11
+ readonly agentName: string;
12
+ write(skill: Skill, options: WriteOptions): Promise<string>;
13
+ read(id: string): Promise<Skill>;
14
+ list(): Promise<InstalledAutomation[]>;
15
+ remove(id: string): Promise<void>;
16
+ }
17
+ export interface WriteOptions {
18
+ cwds?: string[];
19
+ status?: 'ACTIVE' | 'PAUSED';
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare function addCommand(repoSlug: string, options: {
2
+ target?: string;
3
+ cwd?: string;
4
+ }): Promise<void>;
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from 'fs';
2
+ import path from 'path';
3
+ import ora from 'ora';
4
+ import { ClaudeAdapter } from '../adapters/claude.js';
5
+ import { CodexAdapter } from '../adapters/codex.js';
6
+ import { detectAgents } from '../core/detector.js';
7
+ import { fetchFromGitHub } from '../core/github.js';
8
+ import { parseSkillMd } from '../core/skill-parser.js';
9
+ import { addToLock, getLockEntry } from '../lock/lockfile.js';
10
+ import { header, info, success, warn } from '../utils/display.js';
11
+ import { confirm, inputCwd, selectAgents } from '../utils/prompt.js';
12
+ export async function addCommand(repoSlug, options) {
13
+ const spinner = ora('Fetching ' + repoSlug + '...').start();
14
+ let fetchResult;
15
+ try {
16
+ fetchResult = await fetchFromGitHub(repoSlug);
17
+ spinner.stop();
18
+ }
19
+ catch (err) {
20
+ const message = err instanceof Error ? err.message : String(err);
21
+ spinner.fail(message);
22
+ process.exit(1);
23
+ }
24
+ if (!fetchResult)
25
+ return;
26
+ const raw = readFileSync(fetchResult.skillMdPath, 'utf-8');
27
+ const { meta, prompt } = parseSkillMd(raw);
28
+ const auxiliaryFiles = new Map();
29
+ for (const file of fetchResult.files) {
30
+ const basename = path.basename(file);
31
+ if (basename !== 'SKILL.md' && basename !== 'README.md' && basename !== 'LICENSE') {
32
+ const fullPath = path.join(fetchResult.directory, basename);
33
+ try {
34
+ auxiliaryFiles.set(basename, readFileSync(fullPath, 'utf-8'));
35
+ }
36
+ catch {
37
+ // 텍스트 파일만 포함
38
+ }
39
+ }
40
+ }
41
+ const skill = { meta, prompt, auxiliaryFiles };
42
+ header(meta.name);
43
+ info(meta.description || '');
44
+ if (meta.schedule)
45
+ info('Schedule: ' + meta.schedule);
46
+ console.log();
47
+ const existing = getLockEntry(meta.name);
48
+ if (existing) {
49
+ warn(meta.name + ' is already installed');
50
+ const shouldUpdate = await confirm('Update to latest?');
51
+ if (!shouldUpdate)
52
+ return;
53
+ }
54
+ const agents = detectAgents();
55
+ info('Detected agents:');
56
+ for (const agent of agents) {
57
+ const version = agent.version ? ' v' + agent.version : '';
58
+ info(' ' + (agent.installed ? '✓' : '✗') + ' ' + agent.name + version);
59
+ }
60
+ console.log();
61
+ let selectedAgents;
62
+ if (options.target) {
63
+ const targetIds = options.target.split(',');
64
+ selectedAgents = agents.filter((agent) => targetIds.includes(agent.id) && agent.installed);
65
+ }
66
+ else {
67
+ selectedAgents = await selectAgents(agents);
68
+ }
69
+ if (selectedAgents.length === 0) {
70
+ warn('No agents selected');
71
+ return;
72
+ }
73
+ const cwd = options.cwd || (await inputCwd());
74
+ const adapters = selectedAgents.map((agent) => {
75
+ if (agent.id === 'claude')
76
+ return new ClaudeAdapter();
77
+ return new CodexAdapter();
78
+ });
79
+ console.log();
80
+ info('Creating files...');
81
+ const targets = {};
82
+ for (const adapter of adapters) {
83
+ try {
84
+ const createdPath = await adapter.write(skill, { cwds: [cwd] });
85
+ success(adapter.agentName + ' ' + createdPath);
86
+ targets[adapter.agentId] = createdPath;
87
+ }
88
+ catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ warn(adapter.agentName + ' ' + message);
91
+ }
92
+ }
93
+ addToLock(meta.name, {
94
+ source: repoSlug,
95
+ installed_at: new Date().toISOString(),
96
+ targets
97
+ });
98
+ success('Lock file updated');
99
+ console.log();
100
+ info('Done! Installed ' + meta.name + ' → ' + selectedAgents.length + ' agent(s)');
101
+ }
@@ -0,0 +1,3 @@
1
+ export declare function exportCommand(id: string, options: {
2
+ output?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import path from 'path';
3
+ import { ClaudeAdapter } from '../adapters/claude.js';
4
+ import { CodexAdapter } from '../adapters/codex.js';
5
+ import { serializeSkillMd } from '../core/skill-parser.js';
6
+ import { error, header, info, success } from '../utils/display.js';
7
+ export async function exportCommand(id, options) {
8
+ const claudeAdapter = new ClaudeAdapter();
9
+ const codexAdapter = new CodexAdapter();
10
+ let skill;
11
+ let sourceAgent = '';
12
+ try {
13
+ skill = await codexAdapter.read(id);
14
+ sourceAgent = 'Codex App';
15
+ }
16
+ catch {
17
+ try {
18
+ skill = await claudeAdapter.read(id);
19
+ sourceAgent = 'Claude Code';
20
+ }
21
+ catch {
22
+ error('Automation "' + id + '" not found in any agent');
23
+ process.exit(1);
24
+ }
25
+ }
26
+ if (!skill)
27
+ return;
28
+ header('Exporting: ' + skill.meta.name);
29
+ info('Source: ' + sourceAgent);
30
+ info('Description: ' + skill.meta.description);
31
+ if (skill.meta.schedule)
32
+ info('Schedule: ' + skill.meta.schedule);
33
+ console.log();
34
+ const outDir = options.output || path.join(process.cwd(), skill.meta.name);
35
+ mkdirSync(outDir, { recursive: true });
36
+ const content = serializeSkillMd(skill.meta, skill.prompt);
37
+ writeFileSync(path.join(outDir, 'SKILL.md'), content, 'utf-8');
38
+ success('SKILL.md');
39
+ for (const [filename, fileContent] of skill.auxiliaryFiles) {
40
+ writeFileSync(path.join(outDir, filename), fileContent, 'utf-8');
41
+ success(filename);
42
+ }
43
+ console.log();
44
+ info('Exported to ' + outDir);
45
+ info('Push to GitHub and share: npx agentcron add <your-username>/' + skill.meta.name);
46
+ }
@@ -0,0 +1 @@
1
+ export declare function listCommand(): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import chalk from 'chalk';
2
+ import { ClaudeAdapter } from '../adapters/claude.js';
3
+ import { CodexAdapter } from '../adapters/codex.js';
4
+ import { readLock } from '../lock/lockfile.js';
5
+ import { header, info } from '../utils/display.js';
6
+ export async function listCommand() {
7
+ const claudeAdapter = new ClaudeAdapter();
8
+ const codexAdapter = new CodexAdapter();
9
+ const [claudeList, codexList] = await Promise.all([claudeAdapter.list(), codexAdapter.list()]);
10
+ const all = [...claudeList, ...codexList];
11
+ if (all.length === 0) {
12
+ info('No automations found.');
13
+ info('Install one: npx agentcron add <user/repo>');
14
+ return;
15
+ }
16
+ const lock = readLock();
17
+ header('Installed Automations');
18
+ console.log(' ' +
19
+ chalk.dim('Name'.padEnd(30)) +
20
+ ' ' +
21
+ chalk.dim('Agent'.padEnd(12)) +
22
+ ' ' +
23
+ chalk.dim('Status'.padEnd(10)) +
24
+ ' ' +
25
+ chalk.dim('Source'));
26
+ console.log(' ' + '─'.repeat(80));
27
+ for (const item of all) {
28
+ const statusText = item.status === 'ACTIVE'
29
+ ? chalk.green('active')
30
+ : item.status === 'PAUSED'
31
+ ? chalk.yellow('paused')
32
+ : chalk.dim('unknown');
33
+ const source = String(lock.automations[item.name]?.source || chalk.dim('local'));
34
+ const agentText = item.agent === 'claude' ? chalk.blue('Claude') : chalk.cyan('Codex');
35
+ console.log(' ' +
36
+ item.name.padEnd(30) +
37
+ ' ' +
38
+ agentText +
39
+ ' '.repeat(Math.max(0, 12 - item.agent.length)) +
40
+ ' ' +
41
+ statusText +
42
+ ' '.repeat(10) +
43
+ ' ' +
44
+ source);
45
+ }
46
+ console.log('\n ' + chalk.dim('Total: ' + all.length + ' automation(s)'));
47
+ }
@@ -0,0 +1 @@
1
+ export declare function removeCommand(name: string): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { ClaudeAdapter } from '../adapters/claude.js';
2
+ import { CodexAdapter } from '../adapters/codex.js';
3
+ import { getLockEntry, removeFromLock } from '../lock/lockfile.js';
4
+ import { confirm } from '../utils/prompt.js';
5
+ import { error, info, success, warn } from '../utils/display.js';
6
+ export async function removeCommand(name) {
7
+ const lockEntry = getLockEntry(name);
8
+ if (!lockEntry) {
9
+ warn('"' + name + '" not in lock file, searching agents...');
10
+ }
11
+ const ok = await confirm('Remove "' + name + '" from all agents?');
12
+ if (!ok)
13
+ return;
14
+ const claudeAdapter = new ClaudeAdapter();
15
+ const codexAdapter = new CodexAdapter();
16
+ let removed = 0;
17
+ if (lockEntry?.targets?.claude || !lockEntry) {
18
+ try {
19
+ await claudeAdapter.remove(name);
20
+ success('Removed from Claude Code');
21
+ removed++;
22
+ }
23
+ catch {
24
+ // 없으면 무시
25
+ }
26
+ }
27
+ if (lockEntry?.targets?.codex || !lockEntry) {
28
+ try {
29
+ await codexAdapter.remove(name);
30
+ success('Removed from Codex App');
31
+ removed++;
32
+ }
33
+ catch {
34
+ // 없으면 무시
35
+ }
36
+ }
37
+ removeFromLock(name);
38
+ if (removed > 0) {
39
+ success('Lock file updated');
40
+ info('Done! Removed "' + name + '" from ' + removed + ' agent(s)');
41
+ }
42
+ else {
43
+ error('"' + name + '" not found in any agent');
44
+ }
45
+ }
@@ -0,0 +1,8 @@
1
+ export interface AgentInfo {
2
+ name: string;
3
+ id: 'claude' | 'codex';
4
+ installed: boolean;
5
+ version?: string;
6
+ configPath: string;
7
+ }
8
+ export declare function detectAgents(): AgentInfo[];
@@ -0,0 +1,32 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import path from 'path';
5
+ export function detectAgents() {
6
+ const home = homedir();
7
+ const agents = [
8
+ {
9
+ name: 'Claude Code',
10
+ id: 'claude',
11
+ installed: existsSync(path.join(home, '.claude')),
12
+ configPath: path.join(home, '.claude'),
13
+ version: tryGetVersion('claude --version')
14
+ },
15
+ {
16
+ name: 'Codex CLI',
17
+ id: 'codex',
18
+ installed: existsSync(path.join(home, '.codex')),
19
+ configPath: path.join(home, '.codex'),
20
+ version: tryGetVersion('codex --version')
21
+ }
22
+ ];
23
+ return agents;
24
+ }
25
+ function tryGetVersion(cmd) {
26
+ try {
27
+ return execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim().split('\n')[0];
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ export interface FetchResult {
2
+ skillMdPath: string;
3
+ directory: string;
4
+ files: string[];
5
+ }
6
+ export declare function fetchFromGitHub(repoSlug: string): Promise<FetchResult>;
@@ -0,0 +1,48 @@
1
+ import { createWriteStream, mkdirSync, readdirSync } from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { Readable } from 'stream';
5
+ import { pipeline } from 'stream/promises';
6
+ import * as tar from 'tar';
7
+ export async function fetchFromGitHub(repoSlug) {
8
+ const [repo, branch = 'main'] = repoSlug.split('#');
9
+ const tarballUrl = 'https://github.com/' + repo + '/archive/refs/heads/' + branch + '.tar.gz';
10
+ const tmpDir = path.join(os.tmpdir(), 'agentcron-' + Date.now());
11
+ mkdirSync(tmpDir, { recursive: true });
12
+ const response = await fetch(tarballUrl);
13
+ if (!response.ok) {
14
+ throw new Error('Failed to fetch ' + repo + ': ' + response.status + ' ' + response.statusText);
15
+ }
16
+ if (!response.body) {
17
+ throw new Error('Failed to fetch ' + repo + ': empty response body');
18
+ }
19
+ const tarPath = path.join(tmpDir, 'repo.tar.gz');
20
+ const fileStream = createWriteStream(tarPath);
21
+ await pipeline(Readable.fromWeb(response.body), fileStream);
22
+ const extractDir = path.join(tmpDir, 'extracted');
23
+ mkdirSync(extractDir, { recursive: true });
24
+ await tar.x({ file: tarPath, cwd: extractDir, strip: 1 });
25
+ const files = listFilesRecursive(extractDir);
26
+ const skillMdPath = files.find((file) => path.basename(file) === 'SKILL.md');
27
+ if (!skillMdPath) {
28
+ throw new Error('No SKILL.md found in ' + repo);
29
+ }
30
+ return {
31
+ skillMdPath,
32
+ directory: path.dirname(skillMdPath),
33
+ files: files.map((file) => path.relative(extractDir, file))
34
+ };
35
+ }
36
+ function listFilesRecursive(dir) {
37
+ const results = [];
38
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
39
+ const fullPath = path.join(dir, entry.name);
40
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
41
+ results.push(...listFilesRecursive(fullPath));
42
+ }
43
+ else if (entry.isFile()) {
44
+ results.push(fullPath);
45
+ }
46
+ }
47
+ return results;
48
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isCron(str: string): boolean;
2
+ export declare function isRRule(str: string): boolean;
3
+ export declare function cronToRRule(cron: string): string;
4
+ export declare function rruleToCron(rruleStr: string): string;
@@ -0,0 +1,81 @@
1
+ function expandCronField(field) {
2
+ const results = [];
3
+ for (const part of field.split(',')) {
4
+ if (part.includes('-')) {
5
+ const [start, end] = part.split('-').map(Number);
6
+ for (let i = start; i <= end; i += 1)
7
+ results.push(i);
8
+ }
9
+ else if (part !== '*') {
10
+ results.push(Number(part));
11
+ }
12
+ }
13
+ return results;
14
+ }
15
+ export function isCron(str) {
16
+ const parts = str.trim().split(/\s+/);
17
+ return parts.length === 5;
18
+ }
19
+ export function isRRule(str) {
20
+ return str.startsWith('RRULE:') || str.startsWith('FREQ=');
21
+ }
22
+ export function cronToRRule(cron) {
23
+ const parts = cron.trim().split(/\s+/);
24
+ const [minute, hour, dom, _month, dow] = parts;
25
+ const dayMap = {
26
+ '0': 'SU',
27
+ '1': 'MO',
28
+ '2': 'TU',
29
+ '3': 'WE',
30
+ '4': 'TH',
31
+ '5': 'FR',
32
+ '6': 'SA',
33
+ '7': 'SU'
34
+ };
35
+ const rruleParts = [];
36
+ if (dow !== '*' && dom === '*') {
37
+ rruleParts.push('FREQ=WEEKLY');
38
+ const days = expandCronField(dow)
39
+ .map((day) => dayMap[String(day)] || '')
40
+ .filter(Boolean);
41
+ if (days.length > 0)
42
+ rruleParts.push('BYDAY=' + days.join(','));
43
+ }
44
+ else if (dom !== '*') {
45
+ rruleParts.push('FREQ=MONTHLY');
46
+ rruleParts.push('BYMONTHDAY=' + dom);
47
+ }
48
+ else {
49
+ rruleParts.push('FREQ=DAILY');
50
+ }
51
+ if (hour !== '*')
52
+ rruleParts.push('BYHOUR=' + hour);
53
+ if (minute !== '*')
54
+ rruleParts.push('BYMINUTE=' + minute);
55
+ return 'RRULE:' + rruleParts.join(';');
56
+ }
57
+ export function rruleToCron(rruleStr) {
58
+ const str = rruleStr.replace('RRULE:', '');
59
+ const params = Object.fromEntries(str.split(';').map((part) => {
60
+ const [key, value = ''] = part.split('=');
61
+ return [key, value];
62
+ }));
63
+ const minute = params.BYMINUTE || '0';
64
+ const hour = params.BYHOUR || '*';
65
+ const dom = params.BYMONTHDAY || '*';
66
+ const month = '*';
67
+ const rruleDayMap = {
68
+ MO: '1',
69
+ TU: '2',
70
+ WE: '3',
71
+ TH: '4',
72
+ FR: '5',
73
+ SA: '6',
74
+ SU: '0'
75
+ };
76
+ let dow = '*';
77
+ if (params.BYDAY) {
78
+ dow = params.BYDAY.split(',').map((day) => rruleDayMap[day] || day).join(',');
79
+ }
80
+ return minute + ' ' + hour + ' ' + dom + ' ' + month + ' ' + dow;
81
+ }
@@ -0,0 +1,19 @@
1
+ export interface SkillMeta {
2
+ name: string;
3
+ description: string;
4
+ schedule?: string;
5
+ model?: string;
6
+ reasoning_effort?: string;
7
+ source_agent?: 'claude' | 'codex';
8
+ [key: string]: unknown;
9
+ }
10
+ export interface Skill {
11
+ meta: SkillMeta;
12
+ prompt: string;
13
+ auxiliaryFiles: Map<string, string>;
14
+ }
15
+ export declare function parseSkillMd(content: string): {
16
+ meta: SkillMeta;
17
+ prompt: string;
18
+ };
19
+ export declare function serializeSkillMd(meta: SkillMeta, prompt: string): string;
@@ -0,0 +1,8 @@
1
+ import matter from 'gray-matter';
2
+ export function parseSkillMd(content) {
3
+ const { data, content: body } = matter(content);
4
+ return { meta: data, prompt: body.trim() };
5
+ }
6
+ export function serializeSkillMd(meta, prompt) {
7
+ return matter.stringify(prompt, meta);
8
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { addCommand } from './commands/add.js';
4
+ import { exportCommand } from './commands/export-cmd.js';
5
+ import { listCommand } from './commands/list.js';
6
+ import { removeCommand } from './commands/remove.js';
7
+ const program = new Command();
8
+ program
9
+ .name('agentcron')
10
+ .description('Share AI agent automations across Claude Code and Codex App')
11
+ .version('0.1.0');
12
+ program
13
+ .command('add <repo>')
14
+ .description('Install an automation from a GitHub repo (user/repo)')
15
+ .option('--target <agents>', 'Comma-separated agent targets (claude,codex)')
16
+ .option('--cwd <path>', 'Working directory for the automation')
17
+ .action(addCommand);
18
+ program
19
+ .command('export <id>')
20
+ .description('Export an existing automation to SKILL.md format')
21
+ .option('-o, --output <dir>', 'Output directory')
22
+ .action(exportCommand);
23
+ program
24
+ .command('list')
25
+ .description('List all installed automations')
26
+ .action(listCommand);
27
+ program
28
+ .command('remove <name>')
29
+ .description('Remove an installed automation')
30
+ .action(removeCommand);
31
+ program.parse();
@@ -0,0 +1,16 @@
1
+ interface LockEntry {
2
+ source: string;
3
+ installed_at: string;
4
+ targets: Record<string, string>;
5
+ version?: string;
6
+ }
7
+ interface LockData {
8
+ version: number;
9
+ automations: Record<string, LockEntry>;
10
+ }
11
+ export declare function readLock(): LockData;
12
+ export declare function writeLock(data: LockData): void;
13
+ export declare function addToLock(name: string, entry: LockEntry): void;
14
+ export declare function removeFromLock(name: string): void;
15
+ export declare function getLockEntry(name: string): LockEntry | undefined;
16
+ export {};
@@ -0,0 +1,28 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import path from 'path';
4
+ const LOCK_DIR = path.join(homedir(), '.agentcron');
5
+ const LOCK_FILE = path.join(LOCK_DIR, 'lock.json');
6
+ export function readLock() {
7
+ if (!existsSync(LOCK_FILE)) {
8
+ return { version: 1, automations: {} };
9
+ }
10
+ return JSON.parse(readFileSync(LOCK_FILE, 'utf-8'));
11
+ }
12
+ export function writeLock(data) {
13
+ mkdirSync(LOCK_DIR, { recursive: true });
14
+ writeFileSync(LOCK_FILE, JSON.stringify(data, null, 2), 'utf-8');
15
+ }
16
+ export function addToLock(name, entry) {
17
+ const lock = readLock();
18
+ lock.automations[name] = entry;
19
+ writeLock(lock);
20
+ }
21
+ export function removeFromLock(name) {
22
+ const lock = readLock();
23
+ delete lock.automations[name];
24
+ writeLock(lock);
25
+ }
26
+ export function getLockEntry(name) {
27
+ return readLock().automations[name];
28
+ }
@@ -0,0 +1,6 @@
1
+ export declare function header(text: string): void;
2
+ export declare function success(text: string): void;
3
+ export declare function warn(text: string): void;
4
+ export declare function error(text: string): void;
5
+ export declare function info(text: string): void;
6
+ export declare function table(rows: [string, string][]): void;
@@ -0,0 +1,22 @@
1
+ import chalk from 'chalk';
2
+ export function header(text) {
3
+ console.log('\n ' + chalk.bold(text) + '\n');
4
+ }
5
+ export function success(text) {
6
+ console.log(' ' + chalk.green('✓') + ' ' + text);
7
+ }
8
+ export function warn(text) {
9
+ console.log(' ' + chalk.yellow('⚠') + ' ' + text);
10
+ }
11
+ export function error(text) {
12
+ console.log(' ' + chalk.red('✗') + ' ' + text);
13
+ }
14
+ export function info(text) {
15
+ console.log(' ' + text);
16
+ }
17
+ export function table(rows) {
18
+ const maxKey = Math.max(...rows.map(([key]) => key.length));
19
+ for (const [key, value] of rows) {
20
+ console.log(' ' + chalk.dim(key.padEnd(maxKey)) + ' ' + value);
21
+ }
22
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentInfo } from '../core/detector.js';
2
+ export declare function selectAgents(agents: AgentInfo[]): Promise<AgentInfo[]>;
3
+ export declare function inputCwd(): Promise<string>;
4
+ export declare function confirm(message: string): Promise<boolean>;
@@ -0,0 +1,42 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ export async function selectAgents(agents) {
4
+ const { selected } = await inquirer.prompt([
5
+ {
6
+ type: 'checkbox',
7
+ name: 'selected',
8
+ message: 'Select target agents',
9
+ choices: agents.map((agent) => ({
10
+ name: agent.name +
11
+ (agent.version ? ' v' + agent.version : '') +
12
+ (!agent.installed ? chalk.dim(' (not installed)') : ''),
13
+ value: agent.id,
14
+ checked: agent.installed,
15
+ disabled: !agent.installed ? 'not installed' : false
16
+ }))
17
+ }
18
+ ]);
19
+ return agents.filter((agent) => selected.includes(agent.id));
20
+ }
21
+ export async function inputCwd() {
22
+ const { cwd } = await inquirer.prompt([
23
+ {
24
+ type: 'input',
25
+ name: 'cwd',
26
+ message: 'Working directory:',
27
+ default: process.cwd()
28
+ }
29
+ ]);
30
+ return cwd;
31
+ }
32
+ export async function confirm(message) {
33
+ const { ok } = await inquirer.prompt([
34
+ {
35
+ type: 'confirm',
36
+ name: 'ok',
37
+ message,
38
+ default: true
39
+ }
40
+ ]);
41
+ return ok;
42
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@1zero/agentcron",
3
+ "version": "0.1.0",
4
+ "description": "Share AI agent automations across Claude Code and Codex App",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "agentcron": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsx src/index.ts",
16
+ "test": "vitest run"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "keywords": [
22
+ "ai",
23
+ "automation",
24
+ "claude",
25
+ "codex",
26
+ "cron",
27
+ "agent"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@iarna/toml": "^2.2.5",
32
+ "chalk": "^5.6.2",
33
+ "commander": "^14.0.3",
34
+ "cron-parser": "^5.5.0",
35
+ "gray-matter": "^4.0.3",
36
+ "inquirer": "^13.4.1",
37
+ "ora": "^9.3.0",
38
+ "rrule": "^2.8.1",
39
+ "tar": "^7.5.13"
40
+ },
41
+ "devDependencies": {
42
+ "@types/inquirer": "^9.0.9",
43
+ "@types/node": "^25.6.0",
44
+ "tsx": "^4.21.0",
45
+ "typescript": "^6.0.2",
46
+ "vitest": "^4.1.4"
47
+ }
48
+ }