Function bodies 90 total
add function · typescript · L33-L89 (57 LOC)src/cli/commands/add.ts
export async function add(args: AddArgs): Promise<AddResult> {
if (!args.name || !args.cron || !args.command || !args.workingDirectory) {
return {
success: false,
configSaved: false,
osRegistered: false,
error: 'Missing required arguments: --name, --cron, --command, --working-directory',
};
}
// Ensure executor is installed
const initResult = await ensureExecutorInstalled();
if (!initResult.success) {
return {
success: false,
configSaved: false,
osRegistered: false,
error: `Failed to install executor: ${initResult.error}`,
};
}
const task = createTask({
name: args.name,
description: args.description,
trigger: { type: 'cron', expression: args.cron, timezone: 'local' },
execution: {
command: args.command,
workingDirectory: args.workingDirectory,
timeout: args.timeout ?? 300,
skipPermissions: args.skipPermissions ?? false,
},
});
const configPath = getGlobalSchegetBinDir function · typescript · L18-L20 (3 LOC)src/cli/commands/init.ts
function getBinDir(): string {
return path.join(os.homedir(), '.claude', 'bin');
}getExecutorPath function · typescript · L22-L24 (3 LOC)src/cli/commands/init.ts
export function getExecutorPath(): string {
return path.join(getBinDir(), 'claude-scheduler-executor.js');
}getShimPath function · typescript · L26-L28 (3 LOC)src/cli/commands/init.ts
export function getShimPath(): string {
return path.join(getBinDir(), 'claude-scheduler-run');
}getExecutorSourcePath function · typescript · L34-L38 (5 LOC)src/cli/commands/init.ts
function getExecutorSourcePath(): string {
// This file is at src/cli/commands/init.ts or dist/cli/commands/init.js
// Executor is at src/cli/executor.ts or dist/cli/executor.js
return path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'executor.js');
}init function · typescript · L53-L78 (26 LOC)src/cli/commands/init.ts
export async function init(): Promise<InitResult> {
const binDir = getBinDir();
const executorDest = getExecutorPath();
const shimDest = getShimPath();
try {
await fs.mkdir(binDir, { recursive: true });
// Copy executor source to stable path
const executorSource = getExecutorSourcePath();
await fs.copyFile(executorSource, executorDest);
// Write bash shim
const shimContent = SHIM_TEMPLATE.replace('{{EXECUTOR_PATH}}', executorDest);
await fs.writeFile(shimDest, shimContent, { mode: 0o755 });
return { success: true, executorPath: executorDest, shimPath: shimDest };
} catch (err) {
return {
success: false,
executorPath: executorDest,
shimPath: shimDest,
error: (err as Error).message,
};
}
}isExecutorInstalled function · typescript · L83-L91 (9 LOC)src/cli/commands/init.ts
export async function isExecutorInstalled(): Promise<boolean> {
try {
await fs.access(getExecutorPath());
await fs.access(getShimPath());
return true;
} catch {
return false;
}
}Same scanner, your repo: https://repobility.com — Repobility
ensureExecutorInstalled function · typescript · L96-L101 (6 LOC)src/cli/commands/init.ts
export async function ensureExecutorInstalled(): Promise<InitResult> {
if (await isExecutorInstalled()) {
return { success: true, executorPath: getExecutorPath(), shimPath: getShimPath() };
}
return init();
}migrate function · typescript · L20-L73 (54 LOC)src/cli/commands/migrate.ts
export async function migrate(): Promise<MigrateResult> {
const initResult = await ensureExecutorInstalled();
if (!initResult.success) {
return {
success: false,
migrated: [],
skipped: [],
removedScripts: [],
errors: [{ taskId: '*', error: `Failed to install executor: ${initResult.error}` }],
};
}
const configPath = getGlobalSchedulesPath();
const config = await loadConfig(configPath);
const shimPath = getShimPath();
const logsDir = getLogsDir();
const migrated: string[] = [];
const skipped: string[] = [];
const removedScripts: string[] = [];
const errors: Array<{ taskId: string; error: string }> = [];
for (const task of config.tasks) {
if (!task.enabled) {
skipped.push(task.id);
continue;
}
// Re-register with OS scheduler pointing to shared executor
try {
await registerTask(task, shimPath);
migrated.push(task.id);
} catch (err) {
errors.push({ taskId: task.id, error: (eremove function · typescript · L27-L75 (49 LOC)src/cli/commands/remove.ts
export async function remove(args: RemoveArgs): Promise<RemoveResult> {
if (!args.id) {
return {
success: false,
configSaved: false,
osUnregistered: false,
error: 'Missing required argument: --id',
};
}
const configPath = getGlobalSchedulesPath();
const config = await loadConfig(configPath);
const task = findTask(config, args.id);
if (!task) {
return {
success: false,
configSaved: false,
osUnregistered: false,
error: `Task not found: ${args.id}`,
};
}
// Remove from config
const updated = removeTask(config, task.id);
await saveConfig(configPath, updated);
// Unregister from OS
try {
await unregisterTask(task.id);
} catch (err) {
return {
success: false,
taskId: task.id,
taskName: task.name,
configSaved: true,
osUnregistered: false,
error: (err as Error).message,
};
}
return {
success: true,
taskId: task.id,
taskName: task.name,
sync function · typescript · L26-L28 (3 LOC)src/cli/commands/sync.ts
export async function sync(
register: (task: ScheduledTask, shimPath: string) => Promise<void>,
options?: { taskId?: string; configPath?: string },update function · typescript · L34-L105 (72 LOC)src/cli/commands/update.ts
export async function update(args: UpdateArgs): Promise<UpdateResult> {
if (!args.id) {
return {
success: false,
configSaved: false,
osReregistered: false,
error: 'Missing required argument: --id',
};
}
const configPath = getGlobalSchedulesPath();
const config = await loadConfig(configPath);
const existing = findTask(config, args.id);
if (!existing) {
return {
success: false,
configSaved: false,
osReregistered: false,
error: `Task not found: ${args.id}`,
};
}
// Build updates
const updates: Partial<Pick<ScheduledTask, 'name' | 'description' | 'enabled' | 'trigger' | 'execution'>> = {};
if (args.name !== undefined) updates.name = args.name;
if (args.description !== undefined) updates.description = args.description;
if (args.enabled !== undefined) updates.enabled = args.enabled;
if (args.cron !== undefined) {
updates.trigger = { type: 'cron', expression: args.cron, timezone: existing.triggefileMtime function · typescript · L31-L38 (8 LOC)src/cli/executor.ts
async function fileMtime(filePath: string): Promise<number> {
try {
const s = await stat(filePath);
return Math.floor(s.mtimeMs / 1000);
} catch {
return 0;
}
}acquireLock function · typescript · L40-L86 (47 LOC)src/cli/executor.ts
async function acquireLock(taskId: string, timeout: number): Promise<string> {
const lockDir = `/tmp/claude-scheduler-${taskId}.lock`;
try {
await mkdir(lockDir);
} catch {
// Lock exists — check if stale
const now = Math.floor(Date.now() / 1000);
const mtime = await fileMtime(lockDir);
const age = now - mtime;
if (age > timeout + 60) {
// Check if holding process is alive
try {
const pidStr = await readFile(path.join(lockDir, 'pid'), 'utf-8');
const pid = parseInt(pidStr.trim(), 10);
if (!isNaN(pid)) {
try {
process.kill(pid, 0);
// Process is alive — skip
throw new Error(`Task ${taskId} still running (PID ${pid}), skipping.`);
} catch (e) {
if ((e as NodeJS.ErrnoException).code !== 'ESRCH') throw e;
// Process is dead — stale lock
}
}
} catch (e) {
if ((e as Error).message.includes('skipping')) throw e;
releaseLock function · typescript · L88-L90 (3 LOC)src/cli/executor.ts
async function releaseLock(lockDir: string): Promise<void> {
await rm(lockDir, { recursive: true, force: true });
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
spawnClaude function · typescript · L99-L154 (56 LOC)src/cli/executor.ts
function spawnClaude(
command: string,
options: {
cwd: string;
skipPermissions: boolean;
env?: Record<string, string>;
stdoutPath: string;
stderrPath: string;
timeout: number;
},
): Promise<SpawnResult> {
return new Promise((resolve) => {
const { createWriteStream } = require('node:fs') as typeof import('node:fs');
const stdoutStream = createWriteStream(options.stdoutPath);
const stderrStream = createWriteStream(options.stderrPath);
const args = ['-p'];
if (options.skipPermissions) {
args.push('--dangerously-skip-permissions');
}
args.push(command);
const childEnv = { ...process.env, ...(options.env ?? {}) };
const child = spawn('claude', args, {
cwd: options.cwd,
env: childEnv,
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout?.pipe(stdoutStream);
child.stderr?.pipe(stderrStream);
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
childwriteStatus function · typescript · L158-L160 (3 LOC)src/cli/executor.ts
async function writeStatus(logsDir: string, taskId: string, status: string): Promise<void> {
await writeFile(path.join(logsDir, `${taskId}.status`), status, 'utf-8');
}runDirect function · typescript · L164-L180 (17 LOC)src/cli/executor.ts
async function runDirect(
task: ScheduledTask,
logsDir: string,
): Promise<SpawnResult> {
const logPaths = getLogPaths(logsDir, task.id, process.platform);
const stdoutPath = logPaths.stdout ?? logPaths.combined ?? path.join(logsDir, `${task.id}.out.log`);
const stderrPath = logPaths.stderr ?? path.join(logsDir, `${task.id}.err.log`);
return spawnClaude(task.execution.command, {
cwd: task.execution.workingDirectory,
skipPermissions: task.execution.skipPermissions,
env: task.execution.env,
stdoutPath,
stderrPath,
timeout: task.execution.timeout,
});
}runWorktree function · typescript · L184-L187 (4 LOC)src/cli/executor.ts
async function runWorktree(
task: ScheduledTask,
logsDir: string,
): Promise<SpawnResult & { worktreePath?: string; worktreeBranch?: string; pushed?: boolean }> {run function · typescript · L232-L324 (93 LOC)src/cli/executor.ts
export async function run(taskId: string): Promise<void> {
const configPath = process.env.CLAUDE_SCHEDULER_CONFIG
?? path.join(os.homedir(), '.claude', 'schedules.json');
const config = await loadConfig(configPath);
const task = findTask(config, taskId);
if (!task) {
const logsDir = getLogsDir();
await ensureLogsDir(logsDir);
await writeStatus(logsDir, taskId, 'failure:config-error');
process.exitCode = 1;
console.error(`Task not found: ${taskId}`);
return;
}
if (!task.enabled) {
console.error(`Task disabled: ${taskId}`);
return;
}
// Restore PATH if stored
if ((task as Record<string, unknown>).userPath) {
process.env.PATH = (task as Record<string, unknown>).userPath as string;
}
// Set custom env vars
if (task.execution.env) {
for (const [key, value] of Object.entries(task.execution.env)) {
process.env[key] = value;
}
}
const logsDir = getLogsDir();
await ensureLogsDir(logsDir);
let lockDir: smain function · typescript · L11-L140 (130 LOC)src/cli/index.ts
async function main() {
const args = process.argv.slice(2);
const subcommand = args[0];
if (!subcommand) {
console.error('Usage: claude-scheduler <subcommand> [options]');
console.error('Subcommands: init, sync, add, remove, update, migrate');
process.exit(1);
}
try {
switch (subcommand) {
case 'init': {
const result = await init();
console.log(JSON.stringify(result));
process.exitCode = result.success ? 0 : 1;
break;
}
case 'sync': {
const { values } = parseArgs({
args: args.slice(1),
options: {
id: { type: 'string' },
config: { type: 'string' },
},
strict: false,
});
// Dynamic import to avoid loading platform code eagerly
const { registerTask } = await import('./platform.js');
const result = await sync(registerTask, {
taskId: values.id as string | undefined,
configPath: values.confiregisterTask function · typescript · L23-L32 (10 LOC)src/cli/platform.ts
export async function registerTask(task: ScheduledTask, shimPath: string): Promise<void> {
const platform = process.platform;
if (platform === 'darwin') {
await registerDarwin(task, shimPath);
} else if (platform === 'linux') {
await registerLinux(task, shimPath);
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
}unregisterTask function · typescript · L37-L44 (8 LOC)src/cli/platform.ts
export async function unregisterTask(taskId: string): Promise<void> {
const platform = process.platform;
if (platform === 'darwin') {
await unregisterDarwin(taskId);
} else if (platform === 'linux') {
await unregisterLinux(taskId);
}
}Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
registerDarwin function · typescript · L48-L76 (29 LOC)src/cli/platform.ts
async function registerDarwin(task: ScheduledTask, shimPath: string): Promise<void> {
const logsDir = getLogsDir();
const cronExpr = task.trigger.type === 'cron' ? task.trigger.expression : undefined;
const darwinTask: DarwinSchedulerTask = {
id: task.id,
name: task.name,
command: task.execution.command,
workingDirectory: task.execution.workingDirectory,
timeout: task.execution.timeout,
skipPermissions: task.execution.skipPermissions,
logsDir,
userPath: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin',
wrapperScriptPath: shimPath,
cronExpression: cronExpr,
runAtLoad: task.trigger.type === 'once',
};
const plistContent = generatePlist(darwinTask);
const plistPath = getPlistPath(task.id);
// Unload existing if present (ignore errors)
try {
await defaultExec('launchctl', ['unload', plistPath]);
} catch { /* not loaded */ }
await fs.writeFile(plistPath, plistContent, 'utf-8');
await defaultExec('launchctl', ['load'unregisterDarwin function · typescript · L78-L86 (9 LOC)src/cli/platform.ts
async function unregisterDarwin(taskId: string): Promise<void> {
const plistPath = getPlistPath(taskId);
try {
await defaultExec('launchctl', ['unload', plistPath]);
} catch { /* not loaded */ }
try {
await fs.unlink(plistPath);
} catch { /* doesn't exist */ }
}registerLinux function · typescript · L90-L119 (30 LOC)src/cli/platform.ts
async function registerLinux(task: ScheduledTask, shimPath: string): Promise<void> {
const logsDir = getLogsDir();
const cronExpr = task.trigger.type === 'cron' ? task.trigger.expression : undefined;
const linuxTask: LinuxSchedulerTask = {
id: task.id,
name: task.name,
command: task.execution.command,
workingDirectory: task.execution.workingDirectory,
timeout: task.execution.timeout,
skipPermissions: task.execution.skipPermissions,
logsDir,
userPath: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin',
wrapperScriptPath: `${shimPath} ${task.id}`,
cronExpression: cronExpr,
timezone: task.trigger.timezone !== 'local' ? task.trigger.timezone : undefined,
};
let existingCrontab = '';
try {
const result = await defaultExec('crontab', ['-l']);
existingCrontab = result.stdout;
} catch { /* no crontab */ }
const newCrontab = buildCrontabContent(existingCrontab, linuxTask);
const tmpFile = `/tmp/crontab-scheduler-${process.unregisterLinux function · typescript · L121-L133 (13 LOC)src/cli/platform.ts
async function unregisterLinux(taskId: string): Promise<void> {
let existingCrontab = '';
try {
const result = await defaultExec('crontab', ['-l']);
existingCrontab = result.stdout;
} catch { return; /* no crontab */ }
const newCrontab = buildCrontabContent(existingCrontab, null, taskId);
const tmpFile = `/tmp/crontab-scheduler-${process.pid}`;
await fs.writeFile(tmpFile, newCrontab, 'utf-8');
await defaultExec('crontab', [tmpFile]);
await fs.unlink(tmpFile);
}getGlobalSchedulesPath function · typescript · L19-L21 (3 LOC)src/config.ts
export function getGlobalSchedulesPath(): string {
return path.join(os.homedir(), '.claude', 'schedules.json');
}getProjectSchedulesPath function · typescript · L23-L25 (3 LOC)src/config.ts
export function getProjectSchedulesPath(projectPath: string): string {
return path.join(projectPath, '.claude', 'schedules.json');
}getLogsDir function · typescript · L27-L29 (3 LOC)src/config.ts
export function getLogsDir(): string {
return path.join(os.homedir(), '.claude', 'logs');
}getHistoryPath function · typescript · L31-L33 (3 LOC)src/config.ts
export function getHistoryPath(): string {
return path.join(os.homedir(), '.claude', 'execution-history.jsonl');
}Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
getSessionIdPath function · typescript · L39-L44 (6 LOC)src/config.ts
export function getSessionIdPath(taskId: string): string {
if (!isSafeIdentifier(taskId)) {
throw new Error(`Invalid task ID: ${taskId}`);
}
return path.join(getLogsDir(), `${taskId}.session`);
}loadConfig function · typescript · L51-L59 (9 LOC)src/config.ts
export async function loadConfig(filePath: string): Promise<SchedulesConfig> {
try {
const raw = await fs.readFile(filePath, 'utf-8');
const data = JSON.parse(raw);
return SchedulesConfigSchema.parse(data);
} catch {
return createEmptyConfig();
}
}saveConfig function · typescript · L65-L73 (9 LOC)src/config.ts
export async function saveConfig(filePath: string, config: SchedulesConfig): Promise<void> {
// Validate before writing
SchedulesConfigSchema.parse(config);
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
const tmpPath = path.join(dir, `.schedules.${process.pid}.tmp`);
await fs.writeFile(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
await fs.rename(tmpPath, filePath);
}loadMergedConfig function · typescript · L89-L130 (42 LOC)src/config.ts
export async function loadMergedConfig(
projectPath: string,
globalPath?: string,
): Promise<MergedConfigResult> {
const resolvedGlobalPath = globalPath ?? getGlobalSchedulesPath();
const projectConfigPath = getProjectSchedulesPath(projectPath);
const global = await loadConfig(resolvedGlobalPath);
const project = await loadConfig(projectConfigPath);
// Collect global task IDs
const globalTaskIds = new Set(global.tasks.map(t => t.id));
// Sanitize project tasks: strip skipPermissions, drop colliding IDs
const sanitizedProjectTasks: ScheduledTask[] = [];
for (const task of project.tasks) {
if (globalTaskIds.has(task.id)) {
// Global wins on ID collision - silently drop project task
console.warn(`[claude-scheduler] Project task "${task.name}" (${task.id}) dropped: collides with global task ID`);
continue;
}
// Strip skipPermissions from project tasks (trust boundary)
if (task.execution.skipPermissions) {
console.warn(`[claudaddTask function · typescript · L138-L146 (9 LOC)src/config.ts
export function addTask(config: SchedulesConfig, task: ScheduledTask): SchedulesConfig {
if (config.tasks.some(t => t.id === task.id)) {
throw new Error(`Duplicate task ID: ${task.id}`);
}
return {
...config,
tasks: [...config.tasks, task],
};
}updateTask function · typescript · L152-L169 (18 LOC)src/config.ts
export function updateTask(
config: SchedulesConfig,
taskId: string,
updates: Partial<Pick<ScheduledTask, 'name' | 'description' | 'enabled' | 'trigger' | 'execution' | 'tags'>>,
): SchedulesConfig {
const index = config.tasks.findIndex(t => t.id === taskId);
if (index === -1) {
throw new Error(`Task not found: ${taskId}`);
}
const updated: ScheduledTask = {
...config.tasks[index],
...updates,
updatedAt: new Date().toISOString(),
};
const tasks = [...config.tasks];
tasks[index] = updated;
return { ...config, tasks };
}removeTask function · typescript · L175-L184 (10 LOC)src/config.ts
export function removeTask(config: SchedulesConfig, taskId: string): SchedulesConfig {
const index = config.tasks.findIndex(t => t.id === taskId);
if (index === -1) {
throw new Error(`Task not found: ${taskId}`);
}
return {
...config,
tasks: config.tasks.filter(t => t.id !== taskId),
};
}findTask function · typescript · L189-L197 (9 LOC)src/config.ts
export function findTask(config: SchedulesConfig, idOrName: string): ScheduledTask | undefined {
// Try ID match first
const byId = config.tasks.find(t => t.id === idOrName);
if (byId) return byId;
// Fall back to case-insensitive name match
const lowerSearch = idOrName.toLowerCase();
return config.tasks.find(t => t.name.toLowerCase() === lowerSearch);
}Same scanner, your repo: https://repobility.com — Repobility
cronToHuman function · typescript · L11-L17 (7 LOC)src/cron/humanizer.ts
export function cronToHuman(expression: string): string {
try {
return cronstrue.toString(expression, { use24HourTimeFormat: false });
} catch {
return expression;
}
}formatDate function · typescript · L22-L32 (11 LOC)src/cron/humanizer.ts
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
}formatDuration function · typescript · L37-L51 (15 LOC)src/cron/humanizer.ts
export function formatDuration(ms: number): string {
if (ms < 1000) return '0s';
const seconds = Math.floor(ms / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
const parts: string[] = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
return parts.join(' ');
}formatRelativeTime function · typescript · L56-L75 (20 LOC)src/cron/humanizer.ts
export function formatRelativeTime(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
const diffMs = Date.now() - d.getTime();
const diffSeconds = Math.floor(diffMs / 1000);
if (diffSeconds < 60) return 'just now';
const diffMinutes = Math.floor(diffSeconds / 60);
if (diffMinutes < 60) {
return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
}
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) {
return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
}
const diffDays = Math.floor(diffHours / 24);
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
}validateCron function · typescript · L16-L37 (22 LOC)src/cron/parser.ts
export function validateCron(expression: string): ValidationResult {
if (!expression || expression.trim().length === 0) {
return { valid: false, error: 'Empty cron expression' };
}
// Reject 6-field (with seconds) or invalid field counts
const fields = expression.trim().split(/\s+/);
if (fields.length !== 5) {
return { valid: false, error: `Expected 5 fields, got ${fields.length}` };
}
try {
// Croner validates on construction
new Cron(expression);
return { valid: true };
} catch (err) {
return {
valid: false,
error: err instanceof Error ? err.message : 'Invalid cron expression',
};
}
}getNextRuns function · typescript · L42-L58 (17 LOC)src/cron/parser.ts
export function getNextRuns(expression: string, count: number, timezone?: string): Date[] {
const options: Record<string, unknown> = {};
if (timezone && timezone !== 'local') {
options.timezone = timezone;
}
const cron = new Cron(expression, options);
const runs: Date[] = [];
let next = cron.nextRun();
while (next && runs.length < count) {
runs.push(next);
next = cron.nextRun(new Date(next.getTime() + 1000));
}
return runs;
}getNextRun function · typescript · L63-L66 (4 LOC)src/cron/parser.ts
export function getNextRun(expression: string, timezone?: string): Date {
const runs = getNextRuns(expression, 1, timezone);
return runs[0];
}parseHour function · typescript · L80-L92 (13 LOC)src/cron/parser.ts
function parseHour(timeStr: string): number | undefined {
const match = timeStr.match(/(\d{1,2})\s*(am|pm)?/i);
if (!match) return undefined;
let hour = parseInt(match[1], 10);
const meridiem = match[2]?.toLowerCase();
if (meridiem === 'pm' && hour < 12) hour += 12;
if (meridiem === 'am' && hour === 12) hour = 0;
if (hour < 0 || hour > 23) return undefined;
return hour;
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
naturalLanguageToCron function · typescript · L98-L143 (46 LOC)src/cron/parser.ts
export function naturalLanguageToCron(input: string): string | undefined {
const lower = input.toLowerCase().trim();
// Check presets first
for (const [name, expr] of Object.entries(CRON_PRESETS)) {
if (lower === name) return expr;
}
// "every minute"
if (/^every\s+minute$/.test(lower)) {
return '* * * * *';
}
// "every N minutes"
const everyNMin = lower.match(/^every\s+(\d+)\s+minutes?$/);
if (everyNMin) {
const n = parseInt(everyNMin[1], 10);
if (n >= 1 && n <= 59) return `*/${n} * * * *`;
}
// "daily at Xam/pm"
const dailyAt = lower.match(/^daily\s+at\s+(.+)$/);
if (dailyAt) {
const hour = parseHour(dailyAt[1]);
if (hour !== undefined) return `0 ${hour} * * *`;
}
// "every weekday at Xam/pm"
const weekdayAt = lower.match(/^every\s+weekday\s+at\s+(.+)$/);
if (weekdayAt) {
const hour = parseHour(weekdayAt[1]);
if (hour !== undefined) return `0 ${hour} * * 1-5`;
}
// "every <day> at Xam/pm"
const dayAt = lorecordExecution function · typescript · L12-L19 (8 LOC)src/history/index.ts
export async function recordExecution(
historyPath: string,
record: ExecutionHistoryRecord,
): Promise<void> {
await fs.mkdir(path.dirname(historyPath), { recursive: true });
const line = JSON.stringify(record) + '\n';
await fs.appendFile(historyPath, line, 'utf-8');
}getRecentExecutions function · typescript · L34-L84 (51 LOC)src/history/index.ts
export async function getRecentExecutions(
historyPath: string,
options?: QueryOptions,
): Promise<ExecutionHistoryRecord[]> {
let content: string;
try {
content = await fs.readFile(historyPath, 'utf-8');
} catch {
return [];
}
const lines = content.trim().split('\n').filter(l => l.length > 0);
const records: ExecutionHistoryRecord[] = [];
for (const line of lines) {
try {
const record = JSON.parse(line) as ExecutionHistoryRecord;
records.push(record);
} catch {
// Skip corrupted lines
}
}
// Apply filters
let filtered = records;
if (options?.taskId) {
filtered = filtered.filter(r => r.taskId === options.taskId);
}
if (options?.taskName) {
filtered = filtered.filter(r => r.taskName === options.taskName);
}
if (options?.project) {
filtered = filtered.filter(r => r.project === options.project);
}
if (options?.status) {
filtered = filtered.filter(r => r.status === options.status);
}
// Sorpage 1 / 2next ›