Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ For local development setup, see the [Development Guide](https://chriswritescode
- **Git** — Multi-repo support, SSH authentication, worktrees, unified diffs with line numbers, PR creation
- **Files** — Directory browser with tree view, syntax highlighting, create/rename/delete, ZIP download
- **Chat** — Real-time streaming (SSE), slash commands, `@file` mentions, Plan/Build modes, Mermaid diagrams
- **Schedules** — Recurring repo jobs with reusable prompts, run history, linked sessions, and markdown-rendered output
- **Audio** — Text-to-speech (browser + OpenAI-compatible), speech-to-text (browser + OpenAI-compatible)
- **AI** — Model selection, provider config, OAuth for Anthropic/GitHub Copilot, custom agents with system prompts
- **MCP** — Local and remote MCP server support with pre-built templates
Expand Down
4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"private": true,
"scripts": {
"dev": "bun --watch src/index.ts",
"dev": "bun --watch-path src --watch src/index.ts",
"start": "bun src/index.ts",
"build": "bun build src/index.ts --outdir=dist --target=bun",
"typecheck": "tsc --noEmit",
Expand All @@ -20,6 +20,7 @@
"@opencode-manager/shared": "workspace:*",
"archiver": "^7.0.1",
"better-auth": "^1.4.17",
"croner": "^10.0.1",
"dotenv": "^17.2.3",
"eventsource": "^4.1.0",
"hono": "^4.11.7",
Expand All @@ -34,6 +35,7 @@
"@types/bun": "latest",
"@types/eventsource": "^3.0.0",
"@types/web-push": "^3.6.4",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"eslint": "^9.39.1",
"typescript-eslint": "^8.45.0",
Expand Down
58 changes: 58 additions & 0 deletions backend/src/db/migrations/007-schedules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Migration } from '../migration-runner'

const migration: Migration = {
version: 7,
name: 'schedules',

up(db) {
db.run(`
CREATE TABLE IF NOT EXISTS schedule_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_minutes INTEGER,
agent_slug TEXT,
prompt TEXT NOT NULL,
model TEXT,
skill_metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_run_at INTEGER,
next_run_at INTEGER
)
`)

db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_repo ON schedule_jobs(repo_id)')
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_next_run ON schedule_jobs(enabled, next_run_at)')

db.run(`
CREATE TABLE IF NOT EXISTS schedule_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL REFERENCES schedule_jobs(id) ON DELETE CASCADE,
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
trigger_source TEXT NOT NULL,
status TEXT NOT NULL,
started_at INTEGER NOT NULL,
finished_at INTEGER,
created_at INTEGER NOT NULL,
session_id TEXT,
session_title TEXT,
log_text TEXT,
response_text TEXT,
error_text TEXT
)
`)

db.run('CREATE INDEX IF NOT EXISTS idx_schedule_runs_job ON schedule_runs(job_id, started_at DESC)')
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_runs_repo ON schedule_runs(repo_id, started_at DESC)')
},

down(db) {
db.run('DROP TABLE IF EXISTS schedule_runs')
db.run('DROP TABLE IF EXISTS schedule_jobs')
},
}

export default migration
129 changes: 129 additions & 0 deletions backend/src/db/migrations/008-schedule-cron-support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { Migration } from '../migration-runner'

interface ColumnInfo {
name: string
notnull: number
dflt_value: string | null
}

const migration: Migration = {
version: 8,
name: 'schedule-cron-support',

up(db) {
const tableInfo = db.prepare('PRAGMA table_info(schedule_jobs)').all() as ColumnInfo[]
const existingColumns = new Set(tableInfo.map((column) => column.name))
const intervalMinutesColumn = tableInfo.find((column) => column.name === 'interval_minutes')
const scheduleModeColumn = tableInfo.find((column) => column.name === 'schedule_mode')
const hasCronColumns = existingColumns.has('schedule_mode') && existingColumns.has('cron_expression') && existingColumns.has('timezone')
const scheduleModeDefault = scheduleModeColumn?.dflt_value?.replaceAll("'", '')

if (intervalMinutesColumn?.notnull === 0 && hasCronColumns && scheduleModeDefault === 'interval') {
return
}

db.run(`
CREATE TABLE schedule_jobs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_minutes INTEGER,
schedule_mode TEXT NOT NULL DEFAULT 'interval',
cron_expression TEXT,
timezone TEXT,
agent_slug TEXT,
prompt TEXT NOT NULL,
model TEXT,
skill_metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_run_at INTEGER,
next_run_at INTEGER
)
`)

db.run(`
INSERT INTO schedule_jobs_new (
id, repo_id, name, description, enabled, interval_minutes, schedule_mode, cron_expression, timezone,
agent_slug, prompt, model, skill_metadata, created_at, updated_at, last_run_at, next_run_at
)
SELECT
id,
repo_id,
name,
description,
enabled,
interval_minutes,
${existingColumns.has('schedule_mode') ? "COALESCE(schedule_mode, 'interval')" : "'interval'"},
${existingColumns.has('cron_expression') ? 'cron_expression' : 'NULL'},
${existingColumns.has('timezone') ? 'timezone' : 'NULL'},
agent_slug,
prompt,
model,
skill_metadata,
created_at,
updated_at,
last_run_at,
next_run_at
FROM schedule_jobs
`)

db.run('DROP TABLE schedule_jobs')
db.run('ALTER TABLE schedule_jobs_new RENAME TO schedule_jobs')
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_repo ON schedule_jobs(repo_id)')
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_next_run ON schedule_jobs(enabled, next_run_at)')
},

down(db) {
db.run(`
CREATE TABLE schedule_jobs_old (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_minutes INTEGER NOT NULL,
agent_slug TEXT,
prompt TEXT NOT NULL,
model TEXT,
skill_metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_run_at INTEGER,
next_run_at INTEGER
)
`)

db.run(`
INSERT INTO schedule_jobs_old (
id, repo_id, name, description, enabled, interval_minutes, agent_slug, prompt, model, skill_metadata,
created_at, updated_at, last_run_at, next_run_at
)
SELECT
id,
repo_id,
name,
description,
enabled,
COALESCE(interval_minutes, 60),
agent_slug,
prompt,
model,
skill_metadata,
created_at,
updated_at,
last_run_at,
next_run_at
FROM schedule_jobs
`)

db.run('DROP TABLE schedule_jobs')
db.run('ALTER TABLE schedule_jobs_old RENAME TO schedule_jobs')
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_repo ON schedule_jobs(repo_id)')
db.run('CREATE INDEX IF NOT EXISTS idx_schedule_jobs_next_run ON schedule_jobs(enabled, next_run_at)')
},
}

export default migration
Loading
Loading