Skip to content

Commit 1e863c5

Browse files
Add cron-based scheduling with prompt templates integration (#164)
1 parent cbd0ee2 commit 1e863c5

63 files changed

Lines changed: 7797 additions & 64 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ For local development setup, see the [Development Guide](https://chriswritescode
5555
- **Git** — Multi-repo support, SSH authentication, worktrees, unified diffs with line numbers, PR creation
5656
- **Files** — Directory browser with tree view, syntax highlighting, create/rename/delete, ZIP download
5757
- **Chat** — Real-time streaming (SSE), slash commands, `@file` mentions, Plan/Build modes, Mermaid diagrams
58+
- **Schedules** — Recurring repo jobs with reusable prompts, run history, linked sessions, and markdown-rendered output
5859
- **Audio** — Text-to-speech (browser + OpenAI-compatible), speech-to-text (browser + OpenAI-compatible)
5960
- **AI** — Model selection, provider config, OAuth for Anthropic/GitHub Copilot, custom agents with system prompts
6061
- **MCP** — Local and remote MCP server support with pre-built templates

backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"private": true,
66
"scripts": {
7-
"dev": "bun --watch src/index.ts",
7+
"dev": "bun --watch-path src --watch src/index.ts",
88
"start": "bun src/index.ts",
99
"build": "bun build src/index.ts --outdir=dist --target=bun",
1010
"typecheck": "tsc --noEmit",
@@ -20,6 +20,7 @@
2020
"@opencode-manager/shared": "workspace:*",
2121
"archiver": "^7.0.1",
2222
"better-auth": "^1.4.17",
23+
"croner": "^10.0.1",
2324
"dotenv": "^17.2.3",
2425
"eventsource": "^4.1.0",
2526
"hono": "^4.11.7",
@@ -34,6 +35,7 @@
3435
"@types/bun": "latest",
3536
"@types/eventsource": "^3.0.0",
3637
"@types/web-push": "^3.6.4",
38+
"@vitest/coverage-v8": "^3.2.4",
3739
"@vitest/ui": "^3.2.4",
3840
"eslint": "^9.39.1",
3941
"typescript-eslint": "^8.45.0",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { Migration } from '../migration-runner'
2+
3+
const migration: Migration = {
4+
version: 7,
5+
name: 'schedules',
6+
7+
up(db) {
8+
db.run(`
9+
CREATE TABLE IF NOT EXISTS schedule_jobs (
10+
id INTEGER PRIMARY KEY AUTOINCREMENT,
11+
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
12+
name TEXT NOT NULL,
13+
description TEXT,
14+
enabled BOOLEAN NOT NULL DEFAULT TRUE,
15+
interval_minutes INTEGER,
16+
agent_slug TEXT,
17+
prompt TEXT NOT NULL,
18+
model TEXT,
19+
skill_metadata TEXT,
20+
created_at INTEGER NOT NULL,
21+
updated_at INTEGER NOT NULL,
22+
last_run_at INTEGER,
23+
next_run_at INTEGER
24+
)
25+
`)
26+
27+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_repo ON schedule_jobs(repo_id)')
28+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_next_run ON schedule_jobs(enabled, next_run_at)')
29+
30+
db.run(`
31+
CREATE TABLE IF NOT EXISTS schedule_runs (
32+
id INTEGER PRIMARY KEY AUTOINCREMENT,
33+
job_id INTEGER NOT NULL REFERENCES schedule_jobs(id) ON DELETE CASCADE,
34+
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
35+
trigger_source TEXT NOT NULL,
36+
status TEXT NOT NULL,
37+
started_at INTEGER NOT NULL,
38+
finished_at INTEGER,
39+
created_at INTEGER NOT NULL,
40+
session_id TEXT,
41+
session_title TEXT,
42+
log_text TEXT,
43+
response_text TEXT,
44+
error_text TEXT
45+
)
46+
`)
47+
48+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_runs_job ON schedule_runs(job_id, started_at DESC)')
49+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_runs_repo ON schedule_runs(repo_id, started_at DESC)')
50+
},
51+
52+
down(db) {
53+
db.run('DROP TABLE IF EXISTS schedule_runs')
54+
db.run('DROP TABLE IF EXISTS schedule_jobs')
55+
},
56+
}
57+
58+
export default migration
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { Migration } from '../migration-runner'
2+
3+
interface ColumnInfo {
4+
name: string
5+
notnull: number
6+
dflt_value: string | null
7+
}
8+
9+
const migration: Migration = {
10+
version: 8,
11+
name: 'schedule-cron-support',
12+
13+
up(db) {
14+
const tableInfo = db.prepare('PRAGMA table_info(schedule_jobs)').all() as ColumnInfo[]
15+
const existingColumns = new Set(tableInfo.map((column) => column.name))
16+
const intervalMinutesColumn = tableInfo.find((column) => column.name === 'interval_minutes')
17+
const scheduleModeColumn = tableInfo.find((column) => column.name === 'schedule_mode')
18+
const hasCronColumns = existingColumns.has('schedule_mode') && existingColumns.has('cron_expression') && existingColumns.has('timezone')
19+
const scheduleModeDefault = scheduleModeColumn?.dflt_value?.replaceAll("'", '')
20+
21+
if (intervalMinutesColumn?.notnull === 0 && hasCronColumns && scheduleModeDefault === 'interval') {
22+
return
23+
}
24+
25+
db.run(`
26+
CREATE TABLE schedule_jobs_new (
27+
id INTEGER PRIMARY KEY AUTOINCREMENT,
28+
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
29+
name TEXT NOT NULL,
30+
description TEXT,
31+
enabled BOOLEAN NOT NULL DEFAULT TRUE,
32+
interval_minutes INTEGER,
33+
schedule_mode TEXT NOT NULL DEFAULT 'interval',
34+
cron_expression TEXT,
35+
timezone TEXT,
36+
agent_slug TEXT,
37+
prompt TEXT NOT NULL,
38+
model TEXT,
39+
skill_metadata TEXT,
40+
created_at INTEGER NOT NULL,
41+
updated_at INTEGER NOT NULL,
42+
last_run_at INTEGER,
43+
next_run_at INTEGER
44+
)
45+
`)
46+
47+
db.run(`
48+
INSERT INTO schedule_jobs_new (
49+
id, repo_id, name, description, enabled, interval_minutes, schedule_mode, cron_expression, timezone,
50+
agent_slug, prompt, model, skill_metadata, created_at, updated_at, last_run_at, next_run_at
51+
)
52+
SELECT
53+
id,
54+
repo_id,
55+
name,
56+
description,
57+
enabled,
58+
interval_minutes,
59+
${existingColumns.has('schedule_mode') ? "COALESCE(schedule_mode, 'interval')" : "'interval'"},
60+
${existingColumns.has('cron_expression') ? 'cron_expression' : 'NULL'},
61+
${existingColumns.has('timezone') ? 'timezone' : 'NULL'},
62+
agent_slug,
63+
prompt,
64+
model,
65+
skill_metadata,
66+
created_at,
67+
updated_at,
68+
last_run_at,
69+
next_run_at
70+
FROM schedule_jobs
71+
`)
72+
73+
db.run('DROP TABLE schedule_jobs')
74+
db.run('ALTER TABLE schedule_jobs_new RENAME TO schedule_jobs')
75+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_repo ON schedule_jobs(repo_id)')
76+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_next_run ON schedule_jobs(enabled, next_run_at)')
77+
},
78+
79+
down(db) {
80+
db.run(`
81+
CREATE TABLE schedule_jobs_old (
82+
id INTEGER PRIMARY KEY AUTOINCREMENT,
83+
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
84+
name TEXT NOT NULL,
85+
description TEXT,
86+
enabled BOOLEAN NOT NULL DEFAULT TRUE,
87+
interval_minutes INTEGER NOT NULL,
88+
agent_slug TEXT,
89+
prompt TEXT NOT NULL,
90+
model TEXT,
91+
skill_metadata TEXT,
92+
created_at INTEGER NOT NULL,
93+
updated_at INTEGER NOT NULL,
94+
last_run_at INTEGER,
95+
next_run_at INTEGER
96+
)
97+
`)
98+
99+
db.run(`
100+
INSERT INTO schedule_jobs_old (
101+
id, repo_id, name, description, enabled, interval_minutes, agent_slug, prompt, model, skill_metadata,
102+
created_at, updated_at, last_run_at, next_run_at
103+
)
104+
SELECT
105+
id,
106+
repo_id,
107+
name,
108+
description,
109+
enabled,
110+
COALESCE(interval_minutes, 60),
111+
agent_slug,
112+
prompt,
113+
model,
114+
skill_metadata,
115+
created_at,
116+
updated_at,
117+
last_run_at,
118+
next_run_at
119+
FROM schedule_jobs
120+
`)
121+
122+
db.run('DROP TABLE schedule_jobs')
123+
db.run('ALTER TABLE schedule_jobs_old RENAME TO schedule_jobs')
124+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_repo ON schedule_jobs(repo_id)')
125+
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_next_run ON schedule_jobs(enabled, next_run_at)')
126+
},
127+
}
128+
129+
export default migration

0 commit comments

Comments
 (0)