opencode-scheduled-tasks 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 @@
1
+ {"version":3,"sources":["../src/plugin.ts","../src/lib/db.ts","../src/lib/sqlite.ts","../src/lib/tasks.ts","../src/lib/cron.ts","../src/lib/installer.ts"],"sourcesContent":["import { type Plugin, tool } from \"@opencode-ai/plugin\";\nimport { TaskDatabase, getDefaultDbPath } from \"./lib/db.js\";\nimport { readAllTasks, getTasksDir, expandPath, setTaskEnabled } from \"./lib/tasks.js\";\nimport { getNextRunTime, isValidCron } from \"./lib/cron.js\";\nimport { isInstalled } from \"./lib/installer.js\";\n\n\nfunction getDb(): TaskDatabase {\n return new TaskDatabase(getDefaultDbPath());\n}\n\nfunction schedulerWarning(): string {\n if (!isInstalled()) {\n return \"\\n\\nNote: The opencode-scheduler daemon is not installed. Tasks will only execute when the scheduler is run manually. Install it with: npx opencode-scheduler --install\";\n }\n return \"\";\n}\n\nexport const ScheduledTasksPlugin: Plugin = async (ctx) => {\n return {\n tool: {\n schedule_task: tool({\n description:\n \"Schedule a one-off task to run at a specific time. The task will execute an opencode prompt in the specified working directory. Requires the opencode-scheduler daemon to be installed for reliable execution.\",\n args: {\n prompt: tool.schema.string(\n \"The prompt to send to opencode when the task runs\"\n ),\n description: tool.schema.string(\n \"Human-readable description of what this task does\"\n ),\n cwd: tool.schema.string(\n \"Working directory for the task (absolute path or ~ for home)\"\n ),\n scheduled_at: tool.schema.string(\n \"ISO 8601 timestamp for when to run (e.g. '2026-03-31T09:00:00')\"\n ),\n session_name: tool.schema\n .string()\n .optional()\n .describe(\"Session name. If set, reuses the same session across runs. If omitted, creates a fresh session each run.\"),\n model: tool.schema\n .string()\n .optional()\n .describe(\"Model in provider/model format\"),\n agent: tool.schema\n .string()\n .optional()\n .describe(\"Agent to use for execution\"),\n permission: tool.schema\n .string()\n .optional()\n .describe(\n \"Permission config as a JSON string (same schema as opencode.json permissions). Example: '{\\\"bash\\\":{\\\"*\\\":\\\"allow\\\"},\\\"edit\\\":\\\"allow\\\"}'\"\n ),\n },\n async execute(args) {\n // Validate\n const scheduledDate = new Date(args.scheduled_at);\n if (isNaN(scheduledDate.getTime())) {\n return `Error: Invalid date format \"${args.scheduled_at}\". Use ISO 8601 format (e.g. '2026-03-31T09:00:00').`;\n }\n\n if (scheduledDate <= new Date()) {\n return `Error: Scheduled time \"${args.scheduled_at}\" is in the past.`;\n }\n\n const cwd = expandPath(args.cwd);\n\n let permission: any;\n if (args.permission) {\n try {\n permission = JSON.parse(args.permission);\n } catch {\n return `Error: Invalid permission JSON: ${args.permission}`;\n }\n }\n\n const db = getDb();\n try {\n const task = db.createOneoffTask({\n description: args.description,\n prompt: args.prompt,\n cwd,\n scheduledAt: scheduledDate.toISOString(),\n sessionName: args.session_name,\n model: args.model,\n agent: args.agent,\n permission,\n });\n\n return (\n `Task scheduled successfully!\\n` +\n ` ID: ${task.id}\\n` +\n ` Description: ${task.description}\\n` +\n ` Scheduled for: ${task.scheduledAt}\\n` +\n ` Working directory: ${task.cwd}\\n` +\n ` Session: ${task.sessionName ? `named (${task.sessionName})` : \"new (fresh each run)\"}` +\n schedulerWarning()\n );\n } finally {\n db.close();\n }\n },\n }),\n\n list_tasks: tool({\n description:\n \"List all scheduled tasks. Shows recurring tasks from markdown files and pending one-off tasks. Includes next run time for recurring tasks and scheduled time for one-offs.\",\n args: {\n status: tool.schema\n .enum([\"all\", \"pending\", \"completed\", \"failed\"])\n .optional()\n .describe(\"Filter by status (default: all)\"),\n type: tool.schema\n .enum([\"all\", \"recurring\", \"oneoff\"])\n .optional()\n .describe(\"Filter by type (default: all)\"),\n },\n async execute(args) {\n const status = args.status ?? \"all\";\n const type = args.type ?? \"all\";\n const db = getDb();\n const lines: string[] = [];\n\n try {\n // Recurring tasks\n if (type === \"all\" || type === \"recurring\") {\n const { tasks, errors } = readAllTasks();\n\n if (tasks.length > 0) {\n lines.push(\"## Recurring Tasks\\n\");\n for (const task of tasks) {\n const lastRun = db.getLastTaskRun(task.name);\n const statusStr = task.enabled ? \"enabled\" : \"disabled\";\n let nextStr = \"N/A\";\n if (task.enabled) {\n try {\n nextStr = getNextRunTime(task.schedule);\n } catch {\n nextStr = \"invalid cron expression\";\n }\n }\n\n lines.push(`- **${task.name}** (${statusStr})`);\n lines.push(` Schedule: \\`${task.schedule}\\``);\n lines.push(` CWD: ${task.cwd}`);\n lines.push(` Next run: ${nextStr}`);\n if (lastRun) {\n lines.push(\n ` Last run: ${lastRun.status} at ${lastRun.startedAt}`\n );\n } else {\n lines.push(` Last run: never`);\n }\n lines.push(\"\");\n }\n } else {\n lines.push(\"No recurring tasks found.\\n\");\n }\n\n if (errors.length > 0) {\n lines.push(\"### Task file errors:\\n\");\n for (const { file, error } of errors) {\n lines.push(`- ${file}: ${error}`);\n }\n lines.push(\"\");\n }\n }\n\n // One-off tasks\n if (type === \"all\" || type === \"oneoff\") {\n const oneoffs = db.listOneoffTasks({\n status: status === \"all\" ? \"all\" : (status as any),\n });\n\n if (oneoffs.length > 0) {\n lines.push(\"## One-off Tasks\\n\");\n for (const task of oneoffs) {\n lines.push(`- **${task.description}** [${task.status}]`);\n lines.push(` ID: ${task.id}`);\n lines.push(` Scheduled: ${task.scheduledAt}`);\n lines.push(` CWD: ${task.cwd}`);\n if (task.sessionId) {\n lines.push(` Session: ${task.sessionId}`);\n }\n if (task.error) {\n lines.push(` Error: ${task.error}`);\n }\n lines.push(\"\");\n }\n } else {\n lines.push(\n `No one-off tasks found${status !== \"all\" ? ` with status \"${status}\"` : \"\"}.\\n`\n );\n }\n }\n\n return lines.join(\"\\n\") + schedulerWarning();\n } finally {\n db.close();\n }\n },\n }),\n\n cancel_task: tool({\n description:\n \"Cancel a pending one-off task by ID, or disable a recurring task by name.\",\n args: {\n id: tool.schema.string(\n \"Task ID (for one-off, a UUID) or task name (for recurring)\"\n ),\n },\n async execute(args) {\n const db = getDb();\n try {\n // Try as one-off task ID first (UUIDs contain hyphens)\n if (args.id.includes(\"-\")) {\n const task = db.getOneoffTask(args.id);\n if (task) {\n if (task.status !== \"pending\") {\n return `Cannot cancel task: status is \"${task.status}\" (must be \"pending\")`;\n }\n db.cancelOneoffTask(args.id);\n return `Cancelled one-off task: ${task.description} (${task.id})`;\n }\n }\n\n // Try as recurring task name\n const { tasks } = readAllTasks();\n const recurringTask = tasks.find((t) => t.name === args.id);\n if (recurringTask) {\n setTaskEnabled(recurringTask.filePath, false);\n return `Disabled recurring task: ${recurringTask.name}\\nFile updated: ${recurringTask.filePath}`;\n }\n\n return `No task found with ID or name \"${args.id}\"`;\n } finally {\n db.close();\n }\n },\n }),\n\n task_history: tool({\n description:\n \"Get the execution history for a scheduled task. Shows recent runs with status, timing, and any errors.\",\n args: {\n task_name: tool.schema.string(\n \"Task name (for recurring) or task ID (for one-off)\"\n ),\n limit: tool.schema\n .number()\n .optional()\n .describe(\"Maximum number of history entries to show (default: 10)\"),\n },\n async execute(args) {\n const limit = args.limit ?? 10;\n const db = getDb();\n\n try {\n // Try as one-off task\n if (args.task_name.includes(\"-\")) {\n const task = db.getOneoffTask(args.task_name);\n if (task) {\n const lines = [\n `## One-off Task: ${task.description}\\n`,\n `- ID: ${task.id}`,\n `- Status: ${task.status}`,\n `- Scheduled: ${task.scheduledAt}`,\n `- Created: ${task.createdAt}`,\n `- CWD: ${task.cwd}`,\n ];\n if (task.executedAt) lines.push(`- Executed: ${task.executedAt}`);\n if (task.sessionId) lines.push(`- Session: ${task.sessionId}`);\n if (task.error) lines.push(`- Error: ${task.error}`);\n return lines.join(\"\\n\");\n }\n }\n\n // Try as recurring task\n const runs = db.getTaskRunHistory(args.task_name, limit);\n if (runs.length === 0) {\n return `No history found for task \"${args.task_name}\"`;\n }\n\n const lines = [`## History for \"${args.task_name}\"\\n`];\n for (const run of runs) {\n lines.push(`- **${run.status}** at ${run.startedAt}`);\n if (run.completedAt) {\n const duration =\n new Date(run.completedAt).getTime() -\n new Date(run.startedAt).getTime();\n lines.push(` Duration: ${Math.round(duration / 1000)}s`);\n }\n if (run.sessionId) lines.push(` Session: ${run.sessionId}`);\n if (run.error) lines.push(` Error: ${run.error}`);\n }\n\n return lines.join(\"\\n\");\n } finally {\n db.close();\n }\n },\n }),\n\n get_task_instructions: tool({\n description:\n \"Get instructions and the frontmatter format for creating or editing recurring scheduled task markdown files. Use this when the user wants to set up a new recurring task or modify an existing one. After getting instructions, use file tools to create/edit the task file.\",\n args: {},\n async execute() {\n const tasksDir = getTasksDir();\n return `## Creating a Recurring Scheduled Task\n\nRecurring tasks are defined as markdown files in:\n ${tasksDir}\n\nThe filename (without \\`.md\\`) is used as the task name. Each file contains YAML frontmatter followed by the prompt.\n\n### Frontmatter Format\n\n\\`\\`\\`yaml\n---\ndescription: Clean up old branches # Optional. Human-readable description\nschedule: \"0 9 * * *\" # Required. 5-field cron expression\ncwd: ~/projects/my-app # Required. Working directory (~ is expanded)\nsession_name: daily-cleanup # Optional. Reuses the same session across runs. Omit for fresh session each run.\nmodel: anthropic/claude-sonnet-4-6 # Optional. Model to use\nagent: build # Optional. Agent to use\npermission: # Optional. Same format as opencode.json permissions\n bash:\n \"*\": \"allow\"\n \"rm -rf *\": \"deny\"\n edit: \"allow\"\n external_directory: # IMPORTANT for accessing files outside cwd\n \"/tmp/*\": \"allow\"\nenabled: true # Optional. Default: true\n---\n\nThe prompt goes here. This is what will be sent to the opencode agent when the task runs.\n\\`\\`\\`\n\n### Permissions - IMPORTANT\n\nSince scheduled tasks run in the background with no user present, any permission set to \\`\"ask\"\\` will effectively be **denied**. You must explicitly allow any operations the task needs.\n\n**Most commonly missed: \\`external_directory\\`** - This defaults to \\`\"ask\"\\` and controls access to files outside the task's \\`cwd\\`. If your task writes to \\`/tmp\\`, reads from another project, or accesses any path outside \\`cwd\\`, you MUST add an \\`external_directory\\` rule:\n\n\\`\\`\\`yaml\npermission:\n external_directory:\n \"/tmp/*\": \"allow\"\n \"~/other-project/*\": \"allow\"\n\\`\\`\\`\n\nOther permissions like \\`bash\\` and \\`edit\\` default to \\`\"allow\"\\` and usually don't need explicit rules unless you want to restrict them.\n\n### Cron Expression Reference\n\n\\`\\`\\`\n┌───────── minute (0-59)\n│ ┌───────── hour (0-23)\n│ │ ┌───────── day of month (1-31)\n│ │ │ ┌───────── month (1-12)\n│ │ │ │ ┌───────── day of week (0-7, 0 and 7 are Sunday)\n│ │ │ │ │\n* * * * *\n\\`\\`\\`\n\nCommon examples:\n- \\`0 9 * * *\\` - Every day at 9:00 AM\n- \\`0 9 * * 1-5\\` - Every weekday at 9:00 AM\n- \\`*/30 * * * *\\` - Every 30 minutes\n- \\`0 0 * * 0\\` - Every Sunday at midnight\n- \\`0 9 1 * *\\` - First day of every month at 9:00 AM\n\n### Notes\n\n- The scheduler daemon must be installed for tasks to run automatically:\n \\`npx opencode-scheduler --install\\`\n- Tasks use your system's local timezone\n- Tasks with \\`session_name\\` set will reuse the same session across runs\n- Use \\`enabled: false\\` to temporarily disable a task without deleting it${schedulerWarning()}`;\n },\n }),\n },\n\n event: async ({ event }: { event: any }) => {\n if (event.type === \"session.created\") {\n // Opportunistically check for overdue tasks\n try {\n const db = getDb();\n try {\n const overdueTasks = db.getDueOneoffTasks();\n if (overdueTasks.length > 0 && !isInstalled()) {\n await ctx.client.app.log({\n body: {\n service: \"opencode-scheduled-tasks\",\n level: \"warn\",\n message: `${overdueTasks.length} overdue task(s) found but scheduler daemon is not installed. Run: npx opencode-scheduler --install`,\n },\n });\n }\n } finally {\n db.close();\n }\n } catch {\n // Don't let plugin errors crash the session\n }\n }\n },\n };\n};\n\nexport default ScheduledTasksPlugin;\n","import { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { openDatabase, type Database } from \"./sqlite.js\";\nimport type {\n OneoffTask,\n OneoffTaskStatus,\n PermissionConfig,\n SessionMapping,\n TaskRun,\n TaskRunStatus,\n} from \"./types.js\";\n\nconst SCHEMA_VERSION = 2;\n\nconst SCHEMA_V1 = `\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY\n);\n\nCREATE TABLE IF NOT EXISTS oneoff_tasks (\n id TEXT PRIMARY KEY,\n description TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n scheduled_at TEXT NOT NULL,\n session_mode TEXT NOT NULL DEFAULT 'new',\n session_name TEXT,\n model TEXT,\n agent TEXT,\n permission TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n executed_at TEXT,\n session_id TEXT,\n error TEXT,\n created_by_session TEXT\n);\n\nCREATE TABLE IF NOT EXISTS task_runs (\n id TEXT PRIMARY KEY,\n task_name TEXT NOT NULL,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n status TEXT NOT NULL DEFAULT 'running',\n session_id TEXT,\n error TEXT\n);\n\nCREATE TABLE IF NOT EXISTS session_map (\n session_name TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n task_name TEXT,\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n`;\n\nconst MIGRATION_V2 = `\nALTER TABLE task_runs ADD COLUMN pid INTEGER;\nALTER TABLE oneoff_tasks ADD COLUMN pid INTEGER;\n`;\n\nexport class TaskDatabase {\n private db: Database;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n this.db = openDatabase(dbPath);\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"foreign_keys = ON\");\n this.initialize();\n }\n\n private initialize(): void {\n const versionExists = this.db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'\"\n )\n .get();\n\n if (!versionExists) {\n // Fresh database: create all tables, then run migrations\n this.db.exec(SCHEMA_V1);\n this.db\n .prepare(\"INSERT INTO schema_version (version) VALUES (?)\")\n .run(1);\n }\n\n const row = this.db\n .prepare(\"SELECT MAX(version) as version FROM schema_version\")\n .get() as { version: number } | undefined;\n const currentVersion = row?.version ?? 0;\n\n if (currentVersion < SCHEMA_VERSION) {\n this.migrate(currentVersion);\n }\n }\n\n private migrate(fromVersion: number): void {\n if (fromVersion < 2) {\n // V2: Add pid columns for async worker tracking\n // Run each ALTER separately since SQLite doesn't support multi-ALTER\n const statements = MIGRATION_V2.trim().split(\";\").filter(Boolean);\n for (const stmt of statements) {\n try {\n this.db.exec(stmt.trim() + \";\");\n } catch {\n // Column may already exist if migration was partially applied\n }\n }\n this.db\n .prepare(\n \"INSERT OR REPLACE INTO schema_version (version) VALUES (?)\"\n )\n .run(2);\n }\n }\n\n // --- One-off tasks ---\n\n createOneoffTask(task: {\n description: string;\n prompt: string;\n cwd: string;\n scheduledAt: string;\n sessionName?: string;\n model?: string;\n agent?: string;\n permission?: PermissionConfig;\n createdBySession?: string;\n }): OneoffTask {\n const id = randomUUID();\n this.db\n .prepare(\n `INSERT INTO oneoff_tasks (id, description, prompt, cwd, scheduled_at, session_name, model, agent, permission, created_by_session)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(\n id,\n task.description,\n task.prompt,\n task.cwd,\n task.scheduledAt,\n task.sessionName ?? null,\n task.model ?? null,\n task.agent ?? null,\n task.permission ? JSON.stringify(task.permission) : null,\n task.createdBySession ?? null\n );\n return this.getOneoffTask(id)!;\n }\n\n getOneoffTask(id: string): OneoffTask | undefined {\n const row = this.db\n .prepare(\"SELECT * FROM oneoff_tasks WHERE id = ?\")\n .get(id) as any;\n return row ? this.mapOneoffRow(row) : undefined;\n }\n\n listOneoffTasks(options?: {\n status?: OneoffTaskStatus | \"all\";\n }): OneoffTask[] {\n const status = options?.status ?? \"all\";\n let rows: any[];\n if (status === \"all\") {\n rows = this.db\n .prepare(\"SELECT * FROM oneoff_tasks ORDER BY scheduled_at ASC\")\n .all();\n } else {\n rows = this.db\n .prepare(\n \"SELECT * FROM oneoff_tasks WHERE status = ? ORDER BY scheduled_at ASC\"\n )\n .all(status);\n }\n return rows.map((r) => this.mapOneoffRow(r));\n }\n\n getDueOneoffTasks(): OneoffTask[] {\n return this.db\n .prepare(\n \"SELECT * FROM oneoff_tasks WHERE status = 'pending' AND scheduled_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now') ORDER BY scheduled_at ASC\"\n )\n .all()\n .map((r: any) => this.mapOneoffRow(r));\n }\n\n updateOneoffTaskStatus(\n id: string,\n status: OneoffTaskStatus,\n extra?: { sessionId?: string; error?: string; pid?: number }\n ): void {\n const executedAt =\n status === \"running\" ? new Date().toISOString() : undefined;\n this.db\n .prepare(\n `UPDATE oneoff_tasks SET status = ?, executed_at = COALESCE(?, executed_at), session_id = COALESCE(?, session_id), error = ?, pid = COALESCE(?, pid) WHERE id = ?`\n )\n .run(\n status,\n executedAt ?? null,\n extra?.sessionId ?? null,\n extra?.error ?? null,\n extra?.pid ?? null,\n id\n );\n }\n\n setTaskRunPid(id: string, pid: number): void {\n this.db\n .prepare(\"UPDATE task_runs SET pid = ? WHERE id = ?\")\n .run(pid, id);\n }\n\n cancelOneoffTask(id: string): boolean {\n const result = this.db\n .prepare(\n \"UPDATE oneoff_tasks SET status = 'cancelled' WHERE id = ? AND status = 'pending'\"\n )\n .run(id);\n return result.changes > 0;\n }\n\n // --- Task runs (recurring) ---\n\n createTaskRun(taskName: string, pid?: number): TaskRun {\n const id = randomUUID();\n const startedAt = new Date().toISOString();\n this.db\n .prepare(\n \"INSERT INTO task_runs (id, task_name, started_at, pid) VALUES (?, ?, ?, ?)\"\n )\n .run(id, taskName, startedAt, pid ?? null);\n return { id, taskName, startedAt, status: \"running\", pid };\n }\n\n completeTaskRun(\n id: string,\n status: \"completed\" | \"failed\",\n extra?: { sessionId?: string; error?: string }\n ): void {\n const completedAt = new Date().toISOString();\n this.db\n .prepare(\n `UPDATE task_runs SET status = ?, completed_at = ?, session_id = COALESCE(?, session_id), error = ? WHERE id = ?`\n )\n .run(\n status,\n completedAt,\n extra?.sessionId ?? null,\n extra?.error ?? null,\n id\n );\n }\n\n getLastTaskRun(taskName: string): TaskRun | undefined {\n const row = this.db\n .prepare(\n \"SELECT * FROM task_runs WHERE task_name = ? ORDER BY started_at DESC LIMIT 1\"\n )\n .get(taskName) as any;\n return row ? this.mapTaskRunRow(row) : undefined;\n }\n\n getLastSuccessfulTaskRun(taskName: string): TaskRun | undefined {\n const row = this.db\n .prepare(\n \"SELECT * FROM task_runs WHERE task_name = ? AND status = 'completed' ORDER BY started_at DESC LIMIT 1\"\n )\n .get(taskName) as any;\n return row ? this.mapTaskRunRow(row) : undefined;\n }\n\n getTaskRunHistory(taskName: string, limit: number = 10): TaskRun[] {\n return this.db\n .prepare(\n \"SELECT * FROM task_runs WHERE task_name = ? ORDER BY started_at DESC LIMIT ?\"\n )\n .all(taskName, limit)\n .map((r: any) => this.mapTaskRunRow(r));\n }\n\n hasRunningTask(taskName: string): boolean {\n const row = this.db\n .prepare(\n \"SELECT id FROM task_runs WHERE task_name = ? AND status = 'running' LIMIT 1\"\n )\n .get(taskName);\n return !!row;\n }\n\n hasRunningOneoffTask(id: string): boolean {\n const row = this.db\n .prepare(\n \"SELECT id FROM oneoff_tasks WHERE id = ? AND status = 'running' LIMIT 1\"\n )\n .get(id);\n return !!row;\n }\n\n /**\n * Get all running task runs (for PID-based reaping).\n */\n getRunningTaskRuns(): TaskRun[] {\n return this.db\n .prepare(\"SELECT * FROM task_runs WHERE status = 'running'\")\n .all()\n .map((r: any) => this.mapTaskRunRow(r));\n }\n\n /**\n * Get all running one-off tasks (for PID-based reaping).\n */\n getRunningOneoffTasks(): OneoffTask[] {\n return this.db\n .prepare(\"SELECT * FROM oneoff_tasks WHERE status = 'running'\")\n .all()\n .map((r: any) => this.mapOneoffRow(r));\n }\n\n /**\n * Mark stale running records as failed.\n * A record is stale if it has a PID set and that process is no longer alive,\n * or if it has no PID and is older than maxAgeMs (fallback for records\n * created before async execution was added).\n */\n cleanupStaleRuns(maxAgeMs: number = 2 * 60 * 60 * 1000): number {\n const cutoff = new Date(Date.now() - maxAgeMs).toISOString();\n\n // Clean up old records without PIDs (legacy / fallback)\n const taskRunResult = this.db\n .prepare(\n \"UPDATE task_runs SET status = 'failed', completed_at = datetime('now'), error = 'Timed out (stale running record)' WHERE status = 'running' AND pid IS NULL AND started_at < ?\"\n )\n .run(cutoff);\n\n const oneoffResult = this.db\n .prepare(\n \"UPDATE oneoff_tasks SET status = 'failed', error = 'Timed out (stale running record)' WHERE status = 'running' AND pid IS NULL AND executed_at < ?\"\n )\n .run(cutoff);\n\n return taskRunResult.changes + oneoffResult.changes;\n }\n\n // --- Session map ---\n\n getSessionMapping(sessionName: string): SessionMapping | undefined {\n const row = this.db\n .prepare(\"SELECT * FROM session_map WHERE session_name = ?\")\n .get(sessionName) as any;\n return row ? this.mapSessionMapRow(row) : undefined;\n }\n\n upsertSessionMapping(\n sessionName: string,\n sessionId: string,\n taskName?: string\n ): void {\n this.db\n .prepare(\n `INSERT INTO session_map (session_name, session_id, task_name, updated_at)\n VALUES (?, ?, ?, datetime('now'))\n ON CONFLICT(session_name) DO UPDATE SET\n session_id = excluded.session_id,\n task_name = COALESCE(excluded.task_name, session_map.task_name),\n updated_at = datetime('now')`\n )\n .run(sessionName, sessionId, taskName ?? null);\n }\n\n // --- Row mappers ---\n\n private mapOneoffRow(row: any): OneoffTask {\n return {\n id: row.id,\n description: row.description,\n prompt: row.prompt,\n cwd: row.cwd,\n scheduledAt: row.scheduled_at,\n sessionName: row.session_name ?? undefined,\n model: row.model ?? undefined,\n agent: row.agent ?? undefined,\n permission: row.permission ? JSON.parse(row.permission) : undefined,\n status: row.status as OneoffTaskStatus,\n createdAt: row.created_at,\n executedAt: row.executed_at ?? undefined,\n sessionId: row.session_id ?? undefined,\n error: row.error ?? undefined,\n createdBySession: row.created_by_session ?? undefined,\n pid: row.pid ?? undefined,\n };\n }\n\n private mapTaskRunRow(row: any): TaskRun {\n return {\n id: row.id,\n taskName: row.task_name,\n startedAt: row.started_at,\n completedAt: row.completed_at ?? undefined,\n status: row.status as TaskRunStatus,\n sessionId: row.session_id ?? undefined,\n error: row.error ?? undefined,\n pid: row.pid ?? undefined,\n };\n }\n\n private mapSessionMapRow(row: any): SessionMapping {\n return {\n sessionName: row.session_name,\n sessionId: row.session_id,\n taskName: row.task_name ?? undefined,\n updatedAt: row.updated_at,\n };\n }\n\n close(): void {\n this.db.close();\n }\n}\n\n/**\n * Get the default database path\n */\nexport function getDefaultDbPath(): string {\n const home = process.env.HOME ?? process.env.USERPROFILE ?? \"\";\n return `${home}/.config/opencode/.tasks.db`;\n}\n","/**\n * Runtime-agnostic SQLite abstraction.\n *\n * Uses bun:sqlite when running in Bun (OpenCode plugin runtime),\n * falls back to better-sqlite3 when running in Node.js (scheduler CLI).\n */\n\nimport { createRequire } from \"node:module\";\n\nexport interface Statement {\n run(...params: any[]): { changes: number };\n get(...params: any[]): any;\n all(...params: any[]): any[];\n}\n\nexport interface Database {\n exec(sql: string): void;\n prepare(sql: string): Statement;\n pragma(pragma: string): any;\n close(): void;\n}\n\n// Detect runtime\nconst isBun = typeof (globalThis as any).Bun !== \"undefined\";\n\n// createRequire gives us a CJS-style require() that works in ESM context\nconst require = createRequire(import.meta.url);\n\n/**\n * Open a SQLite database using the appropriate runtime driver.\n */\nexport function openDatabase(path: string): Database {\n if (isBun) {\n return openBunDatabase(path);\n }\n return openNodeDatabase(path);\n}\n\nfunction openBunDatabase(path: string): Database {\n // bun:sqlite is available in Bun's runtime\n const { Database: BunDatabase } = require(\"bun:sqlite\");\n const db = new BunDatabase(path);\n\n return {\n exec(sql: string) {\n db.exec(sql);\n },\n prepare(sql: string): Statement {\n const stmt = db.prepare(sql);\n return {\n run(...params: any[]) {\n const result = stmt.run(...params);\n return { changes: result.changes ?? 0 };\n },\n get(...params: any[]) {\n return stmt.get(...params);\n },\n all(...params: any[]) {\n return stmt.all(...params);\n },\n };\n },\n pragma(pragma: string) {\n return db.exec(`PRAGMA ${pragma}`);\n },\n close() {\n db.close();\n },\n };\n}\n\nfunction openNodeDatabase(path: string): Database {\n const BetterSqlite3 = require(\"better-sqlite3\");\n const db = new BetterSqlite3(path);\n\n return {\n exec(sql: string) {\n db.exec(sql);\n },\n prepare(sql: string): Statement {\n const stmt = db.prepare(sql);\n return {\n run(...params: any[]) {\n const result = stmt.run(...params);\n return { changes: result.changes ?? 0 };\n },\n get(...params: any[]) {\n return stmt.get(...params);\n },\n all(...params: any[]) {\n return stmt.all(...params);\n },\n };\n },\n pragma(pragma: string) {\n return db.pragma(pragma);\n },\n close() {\n db.close();\n },\n };\n}\n","import matter from \"gray-matter\";\nimport { readdirSync, readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport type { RecurringTask, TaskFrontmatter } from \"./types.js\";\n\n/**\n * Get the default tasks directory path\n */\nexport function getTasksDir(): string {\n const home = process.env.HOME ?? process.env.USERPROFILE ?? \"\";\n return join(home, \".config\", \"opencode\", \"tasks\");\n}\n\n/**\n * Expand ~ to home directory in a path\n */\nexport function expandPath(p: string): string {\n if (p.startsWith(\"~/\") || p === \"~\") {\n const home = process.env.HOME ?? process.env.USERPROFILE ?? \"\";\n return join(home, p.slice(2));\n }\n return p;\n}\n\n/**\n * Validate task frontmatter and return errors\n */\nfunction validateFrontmatter(\n data: Record<string, any>,\n fileName: string\n): string[] {\n const errors: string[] = [];\n\n if (data.description !== undefined && typeof data.description !== \"string\") {\n errors.push(\"Invalid 'description' field (must be a string)\");\n }\n\n if (!data.schedule || typeof data.schedule !== \"string\") {\n errors.push(\"Missing or invalid 'schedule' field\");\n }\n\n if (!data.cwd || typeof data.cwd !== \"string\") {\n errors.push(\"Missing or invalid 'cwd' field\");\n }\n\n if (data.session_name !== undefined && typeof data.session_name !== \"string\") {\n errors.push(\"Invalid 'session_name' field (must be a string)\");\n }\n\n if (data.model !== undefined && typeof data.model !== \"string\") {\n errors.push(\"Invalid 'model' field (must be a string)\");\n }\n\n if (data.agent !== undefined && typeof data.agent !== \"string\") {\n errors.push(\"Invalid 'agent' field (must be a string)\");\n }\n\n if (data.enabled !== undefined && typeof data.enabled !== \"boolean\") {\n errors.push(\"Invalid 'enabled' field (must be a boolean)\");\n }\n\n return errors;\n}\n\n/**\n * Parse a single task markdown file into a RecurringTask\n */\nexport function parseTaskFile(filePath: string): RecurringTask {\n const content = readFileSync(filePath, \"utf-8\");\n const fileName = basename(filePath);\n const { data, content: body } = matter(content);\n const fm = data as TaskFrontmatter;\n\n const errors = validateFrontmatter(data, fileName);\n if (errors.length > 0) {\n throw new Error(\n `Invalid task file \"${fileName}\":\\n - ${errors.join(\"\\n - \")}`\n );\n }\n\n const name = fileName.replace(/\\.md$/, \"\");\n\n return {\n name,\n description: fm.description,\n schedule: fm.schedule,\n cwd: fm.cwd,\n sessionName: fm.session_name,\n model: fm.model,\n agent: fm.agent,\n permission: fm.permission,\n enabled: fm.enabled ?? true,\n prompt: body.trim(),\n filePath,\n };\n}\n\n/**\n * Read all task files from the tasks directory.\n * Returns successfully parsed tasks and logs errors for invalid ones.\n */\nexport function readAllTasks(\n tasksDir?: string\n): { tasks: RecurringTask[]; errors: Array<{ file: string; error: string }> } {\n const dir = tasksDir ?? getTasksDir();\n const tasks: RecurringTask[] = [];\n const errors: Array<{ file: string; error: string }> = [];\n\n if (!existsSync(dir)) {\n return { tasks, errors };\n }\n\n const files = readdirSync(dir).filter((f) => f.endsWith(\".md\"));\n\n for (const file of files) {\n const filePath = join(dir, file);\n try {\n const task = parseTaskFile(filePath);\n tasks.push(task);\n } catch (err: any) {\n errors.push({ file, error: err.message });\n }\n }\n\n return { tasks, errors };\n}\n\n/**\n * Update the enabled field in a task's frontmatter.\n * Preserves the rest of the file content.\n */\nexport function setTaskEnabled(filePath: string, enabled: boolean): void {\n const content = readFileSync(filePath, \"utf-8\");\n const { data, content: body } = matter(content);\n data.enabled = enabled;\n const updated = matter.stringify(body, data);\n writeFileSync(filePath, updated);\n}\n","import cronParser from \"cron-parser\";\n\n// Handle CJS/ESM interop: in Node ESM, the default import is the module\n// namespace object; the actual class is at .CronExpressionParser or .default\nconst CronExpressionParser =\n (cronParser as any).CronExpressionParser ?? cronParser;\n\n/**\n * Check if a cron expression is valid\n */\nexport function isValidCron(expression: string): boolean {\n try {\n CronExpressionParser.parse(expression);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get the next run time for a cron expression after the given date.\n * Returns an ISO 8601 string.\n */\nexport function getNextRunTime(expression: string, after?: Date): string {\n const expr = CronExpressionParser.parse(expression, {\n currentDate: after ?? new Date(),\n });\n const next = expr.next().toISOString();\n if (!next) throw new Error(`No next run time for expression \"${expression}\"`);\n return next;\n}\n\n/**\n * Get the previous run time for a cron expression before the given date.\n * Returns an ISO 8601 string.\n */\nexport function getPreviousRunTime(\n expression: string,\n before?: Date\n): string {\n const expr = CronExpressionParser.parse(expression, {\n currentDate: before ?? new Date(),\n });\n const prev = expr.prev().toISOString();\n if (!prev) throw new Error(`No previous run time for expression \"${expression}\"`);\n return prev;\n}\n\n/**\n * Determine if a recurring task is due for execution.\n *\n * A task is due if:\n * - It has never run before, OR\n * - The last run was before the most recent cron trigger time\n *\n * For tasks that have never run, we only consider them due if the previous\n * trigger is within the last 24 hours (to avoid running very old tasks on\n * first install).\n *\n * @param expression - Cron expression\n * @param lastRunTime - ISO 8601 timestamp of last successful run, or undefined if never run\n * @returns true if the task should be executed now\n */\nexport function isDue(expression: string, lastRunTime?: string): boolean {\n if (!lastRunTime) {\n // Never run before - check if the cron has a trigger time in the past\n // within the last 24 hours\n const prevTrigger = new Date(getPreviousRunTime(expression));\n const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);\n return prevTrigger >= twentyFourHoursAgo;\n }\n\n // Find the most recent cron trigger time\n const prevTrigger = new Date(getPreviousRunTime(expression));\n const lastRun = new Date(lastRunTime);\n\n // Task is due if the most recent trigger is after the last run\n return prevTrigger > lastRun;\n}\n","import { execFileSync } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type Platform = \"macos-launchd\" | \"linux-systemd\" | \"unsupported\";\n\nconst LAUNCHD_LABEL = \"ai.opencode.scheduled-tasks\";\nconst SYSTEMD_SERVICE = \"opencode-scheduler.service\";\nconst SYSTEMD_TIMER = \"opencode-scheduler.timer\";\n\n/**\n * Detect the platform and init system\n */\nexport function detectPlatform(): Platform {\n if (process.platform === \"darwin\") return \"macos-launchd\";\n if (process.platform === \"linux\") {\n try {\n execFileSync(\"systemctl\", [\"--version\"], { stdio: \"ignore\" });\n return \"linux-systemd\";\n } catch {\n // systemctl not found or not working\n }\n }\n return \"unsupported\";\n}\n\n/**\n * Resolve the absolute path to the CLI script.\n *\n * Since tsup bundles installer.ts into cli.js, import.meta.url\n * already points to the CLI script when running from the bundle.\n * When running from source (ts-node/tsx), we walk up to find dist/cli.js.\n * As a final fallback, we look for the `opencode-scheduler` bin on PATH.\n */\nfunction resolveSchedulerPath(): string {\n const thisFile = fileURLToPath(import.meta.url);\n\n // Case 1: We ARE the CLI script (bundled by tsup)\n if (basename(thisFile) === \"cli.js\") {\n return resolve(thisFile);\n }\n\n // Case 2: Running from source (src/lib/installer.ts)\n // Walk up to find dist/cli.js\n const candidates = [\n join(dirname(dirname(thisFile)), \"..\", \"dist\", \"cli.js\"), // from src/lib/\n join(dirname(thisFile), \"..\", \"dist\", \"cli.js\"), // from src/\n join(dirname(dirname(thisFile)), \"cli.js\"), // from dist/lib/ (if unbundled)\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return resolve(candidate);\n }\n }\n\n // Case 3: Fallback to PATH lookup\n try {\n const result = execFileSync(\"which\", [\"opencode-scheduler\"], {\n encoding: \"utf-8\",\n }).trim();\n if (result) return resolve(result);\n } catch {\n // not found\n }\n\n throw new Error(\n \"Could not find the opencode-scheduler script. \" +\n \"Make sure the package is properly installed.\"\n );\n}\n\nfunction getHome(): string {\n return process.env.HOME ?? process.env.USERPROFILE ?? \"\";\n}\n\nfunction getLogDir(): string {\n const dir = join(getHome(), \".local\", \"share\", \"opencode\");\n mkdirSync(dir, { recursive: true });\n return dir;\n}\n\n// --- macOS launchd ---\n\nfunction getLaunchdPlistPath(): string {\n return join(\n getHome(),\n \"Library\",\n \"LaunchAgents\",\n `${LAUNCHD_LABEL}.plist`\n );\n}\n\nfunction generateLaunchdPlist(\n nodePath: string,\n schedulerPath: string\n): string {\n const logDir = getLogDir();\n const currentPath = process.env.PATH ?? \"/usr/local/bin:/usr/bin:/bin\";\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key>\n <string>${LAUNCHD_LABEL}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${nodePath}</string>\n <string>${schedulerPath}</string>\n <string>--run-once</string>\n </array>\n <key>StartInterval</key>\n <integer>60</integer>\n <key>StandardOutPath</key>\n <string>${join(logDir, \"scheduler.log\")}</string>\n <key>StandardErrorPath</key>\n <string>${join(logDir, \"scheduler.err\")}</string>\n <key>RunAtLoad</key>\n <true/>\n <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>${currentPath}</string>\n </dict>\n</dict>\n</plist>`;\n}\n\nasync function installLaunchd(): Promise<void> {\n const nodePath = process.execPath;\n const schedulerPath = resolveSchedulerPath();\n const plistPath = getLaunchdPlistPath();\n\n // Ensure LaunchAgents directory exists\n mkdirSync(dirname(plistPath), { recursive: true });\n\n // Unload if already loaded\n try {\n execFileSync(\"launchctl\", [\"unload\", plistPath], { stdio: \"ignore\" });\n } catch {\n // Not loaded, that's fine\n }\n\n // Write plist\n const plist = generateLaunchdPlist(nodePath, schedulerPath);\n writeFileSync(plistPath, plist);\n\n // Load\n execFileSync(\"launchctl\", [\"load\", plistPath]);\n\n console.log(\"Scheduler installed (macOS launchd)\");\n console.log(` Plist: ${plistPath}`);\n console.log(` Node: ${nodePath}`);\n console.log(` Script: ${schedulerPath}`);\n console.log(` Interval: every 60 seconds`);\n console.log(` Logs: ${getLogDir()}/scheduler.{log,err}`);\n}\n\nasync function uninstallLaunchd(): Promise<void> {\n const plistPath = getLaunchdPlistPath();\n\n if (!existsSync(plistPath)) {\n console.log(\"Scheduler is not installed (no launchd plist found)\");\n return;\n }\n\n try {\n execFileSync(\"launchctl\", [\"unload\", plistPath], { stdio: \"ignore\" });\n } catch {\n // Already unloaded\n }\n\n unlinkSync(plistPath);\n console.log(\"Scheduler uninstalled (macOS launchd)\");\n console.log(` Removed: ${plistPath}`);\n}\n\nfunction isLaunchdInstalled(): boolean {\n return existsSync(getLaunchdPlistPath());\n}\n\n// --- Linux systemd ---\n\nfunction getSystemdDir(): string {\n return join(getHome(), \".config\", \"systemd\", \"user\");\n}\n\nfunction generateSystemdService(\n nodePath: string,\n schedulerPath: string\n): string {\n const currentPath = process.env.PATH ?? \"/usr/local/bin:/usr/bin:/bin\";\n\n return `[Unit]\nDescription=OpenCode Scheduled Tasks Runner\n\n[Service]\nType=oneshot\nExecStart=${nodePath} ${schedulerPath} --run-once\nEnvironment=PATH=${currentPath}\n`;\n}\n\nfunction generateSystemdTimer(): string {\n return `[Unit]\nDescription=OpenCode Scheduled Tasks Timer\n\n[Timer]\nOnBootSec=60\nOnUnitActiveSec=60\nAccuracySec=1s\n\n[Install]\nWantedBy=timers.target\n`;\n}\n\nasync function installSystemd(): Promise<void> {\n const nodePath = process.execPath;\n const schedulerPath = resolveSchedulerPath();\n const systemdDir = getSystemdDir();\n\n mkdirSync(systemdDir, { recursive: true });\n\n const servicePath = join(systemdDir, SYSTEMD_SERVICE);\n const timerPath = join(systemdDir, SYSTEMD_TIMER);\n\n // Stop if already running\n try {\n execFileSync(\"systemctl\", [\"--user\", \"stop\", SYSTEMD_TIMER], {\n stdio: \"ignore\",\n });\n } catch {\n // Not running\n }\n\n // Write unit files\n writeFileSync(servicePath, generateSystemdService(nodePath, schedulerPath));\n writeFileSync(timerPath, generateSystemdTimer());\n\n // Reload, enable, start\n execFileSync(\"systemctl\", [\"--user\", \"daemon-reload\"]);\n execFileSync(\"systemctl\", [\"--user\", \"enable\", SYSTEMD_TIMER]);\n execFileSync(\"systemctl\", [\"--user\", \"start\", SYSTEMD_TIMER]);\n\n console.log(\"Scheduler installed (Linux systemd)\");\n console.log(` Service: ${servicePath}`);\n console.log(` Timer: ${timerPath}`);\n console.log(` Node: ${nodePath}`);\n console.log(` Script: ${schedulerPath}`);\n console.log(` Interval: every 60 seconds`);\n}\n\nasync function uninstallSystemd(): Promise<void> {\n const systemdDir = getSystemdDir();\n const servicePath = join(systemdDir, SYSTEMD_SERVICE);\n const timerPath = join(systemdDir, SYSTEMD_TIMER);\n\n if (!existsSync(timerPath) && !existsSync(servicePath)) {\n console.log(\"Scheduler is not installed (no systemd units found)\");\n return;\n }\n\n try {\n execFileSync(\"systemctl\", [\"--user\", \"stop\", SYSTEMD_TIMER], {\n stdio: \"ignore\",\n });\n execFileSync(\"systemctl\", [\"--user\", \"disable\", SYSTEMD_TIMER], {\n stdio: \"ignore\",\n });\n } catch {\n // Already stopped/disabled\n }\n\n if (existsSync(servicePath)) unlinkSync(servicePath);\n if (existsSync(timerPath)) unlinkSync(timerPath);\n\n try {\n execFileSync(\"systemctl\", [\"--user\", \"daemon-reload\"]);\n } catch {\n // Best effort\n }\n\n console.log(\"Scheduler uninstalled (Linux systemd)\");\n console.log(` Removed: ${servicePath}`);\n console.log(` Removed: ${timerPath}`);\n}\n\nfunction isSystemdInstalled(): boolean {\n const systemdDir = getSystemdDir();\n return existsSync(join(systemdDir, SYSTEMD_TIMER));\n}\n\n// --- Public API ---\n\n/**\n * Install the scheduler for the detected platform\n */\nexport async function install(): Promise<void> {\n const platform = detectPlatform();\n\n switch (platform) {\n case \"macos-launchd\":\n await installLaunchd();\n break;\n case \"linux-systemd\":\n await installSystemd();\n break;\n case \"unsupported\":\n console.error(\n \"Unsupported platform. Supported: macOS (launchd), Linux (systemd).\"\n );\n console.error(\"You can still run the scheduler manually:\");\n console.error(\" npx opencode-scheduler --run-once\");\n process.exit(1);\n }\n}\n\n/**\n * Uninstall the scheduler for the detected platform\n */\nexport async function uninstall(): Promise<void> {\n const platform = detectPlatform();\n\n switch (platform) {\n case \"macos-launchd\":\n await uninstallLaunchd();\n break;\n case \"linux-systemd\":\n await uninstallSystemd();\n break;\n case \"unsupported\":\n console.error(\"No supported init system found.\");\n process.exit(1);\n }\n}\n\n/**\n * Check if the scheduler is installed\n */\nexport function isInstalled(): boolean {\n const platform = detectPlatform();\n switch (platform) {\n case \"macos-launchd\":\n return isLaunchdInstalled();\n case \"linux-systemd\":\n return isSystemdInstalled();\n default:\n return false;\n }\n}\n\n/**\n * Get info about the current installation\n */\nexport function getInstallInfo(): {\n installed: boolean;\n platform: Platform;\n details?: string;\n} {\n const platform = detectPlatform();\n const installed = isInstalled();\n\n let details: string | undefined;\n if (installed) {\n switch (platform) {\n case \"macos-launchd\":\n details = `Plist: ${getLaunchdPlistPath()}`;\n break;\n case \"linux-systemd\":\n details = `Timer: ${join(getSystemdDir(), SYSTEMD_TIMER)}`;\n break;\n }\n }\n\n return { installed, platform, details };\n}\n"],"mappings":"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","names":["require","existsSync","mkdirSync","writeFileSync","basename","dirname","join","join","existsSync","join","existsSync","join","lines"]}
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: Clean up merged git branches
3
+ schedule: "0 9 * * *"
4
+ cwd: ~/projects/my-app
5
+ session_name: daily-cleanup
6
+ permission:
7
+ bash:
8
+ "*": "allow"
9
+ "git push *": "deny"
10
+ edit: "deny"
11
+ enabled: true
12
+ ---
13
+
14
+ Check for local branches that have been merged into main and delete them.
15
+ List any branches that look stale (no commits in >30 days) but haven't been merged yet.
16
+ Do not delete any unmerged branches.
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Generate a weekly summary of project activity
3
+ schedule: "0 8 * * 1"
4
+ cwd: ~/projects/my-app
5
+ model: anthropic/claude-sonnet-4-6
6
+ permission:
7
+ bash:
8
+ "*": "allow"
9
+ edit: "deny"
10
+ enabled: true
11
+ ---
12
+
13
+ Generate a weekly summary of project activity for the past 7 days. Include:
14
+
15
+ 1. All commits with brief descriptions
16
+ 2. Files changed (grouped by directory)
17
+ 3. Any open PRs and their status
18
+ 4. A brief analysis of development velocity and focus areas
19
+
20
+ Format the output as a clean markdown report.
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "opencode-scheduled-tasks",
3
+ "version": "0.1.0",
4
+ "description": "Scheduled task runner plugin for OpenCode - cron-based recurring and one-off task scheduling",
5
+ "type": "module",
6
+ "main": "./dist/plugin.js",
7
+ "module": "./dist/plugin.js",
8
+ "types": "./dist/plugin.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/plugin.js",
12
+ "types": "./dist/plugin.d.ts"
13
+ }
14
+ },
15
+ "bin": {
16
+ "opencode-scheduler": "./dist/cli.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "examples",
21
+ "skill"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "prepublishOnly": "npm run build",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest"
29
+ },
30
+ "keywords": [
31
+ "opencode",
32
+ "scheduler",
33
+ "cron",
34
+ "tasks",
35
+ "automation",
36
+ "plugin"
37
+ ],
38
+ "author": "Jeremy Dormitzer",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/jdormit/opencode-scheduled-tasks.git"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "dependencies": {
48
+ "better-sqlite3": "^11.0.0",
49
+ "cron-parser": "^5.0.0",
50
+ "gray-matter": "^4.0.3"
51
+ },
52
+ "peerDependencies": {
53
+ "@opencode-ai/plugin": ">=0.15.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@opencode-ai/plugin": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@opencode-ai/plugin": "^1.0.0",
62
+ "@opencode-ai/sdk": "^1.0.0",
63
+ "@types/better-sqlite3": "^7.6.0",
64
+ "@types/node": "^22.0.0",
65
+ "tsup": "^8.0.0",
66
+ "typescript": "^5.9.0",
67
+ "vitest": "^3.0.0"
68
+ }
69
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: scheduled-tasks
3
+ description: Create and manage scheduled tasks for OpenCode. Use when the user wants to schedule one-off or recurring tasks, check task status, or learn how to set up recurring task files. Provides guidance on permissions, cron schedules, and the scheduler daemon.
4
+ ---
5
+
6
+ # Scheduled Tasks for OpenCode
7
+
8
+ You have access to tools for scheduling OpenCode tasks. Tasks can be one-off (run once at a specific time) or recurring (defined as markdown files with cron schedules).
9
+
10
+ ## Available Tools
11
+
12
+ - `schedule_task` - Schedule a one-off task to run at a specific time
13
+ - `list_tasks` - List all scheduled tasks (recurring and one-off)
14
+ - `cancel_task` - Cancel a pending one-off task or disable a recurring task
15
+ - `task_history` - Get execution history for a task
16
+ - `get_task_instructions` - Get the full frontmatter format for recurring task files
17
+
18
+ ## Permissions for Scheduled Tasks
19
+
20
+ **This is critical.** Scheduled tasks run via `opencode run` in the background with no user present to approve permission prompts. Any permission set to `"ask"` (the default for some permissions) will effectively be denied.
21
+
22
+ You MUST explicitly set permissions for any operations the task needs to perform. The most commonly needed permissions are:
23
+
24
+ ### `bash` permission
25
+ Required for any shell commands. Default is `"allow"` for most commands.
26
+
27
+ ### `edit` permission
28
+ Required for file modifications. Default is `"allow"`.
29
+
30
+ ### `external_directory` permission
31
+ **This is the most commonly missed permission.** It defaults to `"ask"`, which means any file access outside the task's `cwd` will be silently denied in background execution.
32
+
33
+ If a task needs to read or write files outside its working directory (e.g., writing to `/tmp`, reading from another project), you MUST allow those paths:
34
+
35
+ ```json
36
+ {
37
+ "external_directory": {
38
+ "/tmp/*": "allow",
39
+ "~/other-project/*": "allow"
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### `read` permission
45
+ Required for reading files. Default is `"allow"` except for `.env` files.
46
+
47
+ ### Example: Comprehensive permissions for a task
48
+
49
+ For a one-off task via the `schedule_task` tool, pass permissions as a JSON string:
50
+
51
+ ```json
52
+ {
53
+ "bash": { "*": "allow" },
54
+ "edit": "allow",
55
+ "external_directory": { "/tmp/*": "allow" }
56
+ }
57
+ ```
58
+
59
+ For a recurring task markdown file:
60
+
61
+ ```yaml
62
+ permission:
63
+ bash:
64
+ "*": "allow"
65
+ edit: "allow"
66
+ external_directory:
67
+ "/tmp/*": "allow"
68
+ "~/other-project/*": "allow"
69
+ ```
70
+
71
+ ### Rule of thumb
72
+
73
+ When creating a scheduled task, always ask yourself: "Will this task touch any files outside its `cwd`?" If yes, add `external_directory` rules for those paths.
74
+
75
+ ## One-off Tasks
76
+
77
+ Use the `schedule_task` tool to create tasks that run once at a specific time.
78
+
79
+ Key parameters:
80
+ - `prompt` - What the opencode agent should do
81
+ - `description` - Human-readable label
82
+ - `cwd` - Working directory (absolute path or `~` for home)
83
+ - `scheduled_at` - ISO 8601 timestamp (e.g., `2026-03-31T09:00:00`)
84
+ - `permission` - JSON string with permission config (see above)
85
+ - `session_name` - If set, reuses the same session across runs. Omit for fresh session each run.
86
+ - `model` - Optional model override (e.g., `anthropic/claude-sonnet-4-6`)
87
+ - `agent` - Optional agent override
88
+
89
+ ## Recurring Tasks
90
+
91
+ Recurring tasks are markdown files in `~/.config/opencode/tasks/`. Use `get_task_instructions` to get the full frontmatter format, then create the file using file tools.
92
+
93
+ Key points:
94
+ - The filename (without `.md`) is the task name (e.g., `daily-cleanup.md` -> task name `daily-cleanup`)
95
+ - `schedule` is a 5-field cron expression (minute hour day-of-month month day-of-week)
96
+ - The markdown body is the prompt sent to the agent
97
+ - Set `enabled: false` to temporarily disable without deleting
98
+
99
+ ### Common cron patterns
100
+ - `0 9 * * *` - Every day at 9:00 AM (local time)
101
+ - `0 9 * * 1-5` - Every weekday at 9:00 AM
102
+ - `*/30 * * * *` - Every 30 minutes
103
+ - `0 0 * * 0` - Every Sunday at midnight
104
+ - `0 9 1 * *` - First day of every month at 9:00 AM
105
+
106
+ ## Scheduler Daemon
107
+
108
+ Tasks only execute when the scheduler daemon is installed. It runs every 60 seconds via launchd (macOS) or systemd (Linux).
109
+
110
+ Install: `npx opencode-scheduler --install`
111
+ Uninstall: `npx opencode-scheduler --uninstall`
112
+ Check status: `npx opencode-scheduler --status`
113
+
114
+ If the daemon is not installed, warn the user and suggest they install it.
115
+
116
+ ## Session Behavior
117
+
118
+ - By default (no `session_name`), each run creates a fresh session. Good for independent tasks.
119
+ - If `session_name` is set, the same session is reused across runs. Good for tasks that build on previous context (e.g., a daily standup that references yesterday's work).