@covibes/zeroshot 5.2.1 → 5.4.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +226 -195
  3. package/cli/commands/providers.js +149 -0
  4. package/cli/index.js +3145 -2366
  5. package/cli/lib/first-run.js +40 -3
  6. package/cli/message-formatters-normal.js +28 -6
  7. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  8. package/cluster-templates/base-templates/full-workflow.json +99 -316
  9. package/cluster-templates/base-templates/single-worker.json +23 -15
  10. package/cluster-templates/base-templates/worker-validator.json +105 -36
  11. package/cluster-templates/conductor-bootstrap.json +9 -7
  12. package/lib/docker-config.js +14 -1
  13. package/lib/git-remote-utils.js +165 -0
  14. package/lib/id-detector.js +10 -7
  15. package/lib/provider-defaults.js +62 -0
  16. package/lib/provider-detection.js +59 -0
  17. package/lib/provider-names.js +57 -0
  18. package/lib/settings/claude-auth.js +78 -0
  19. package/lib/settings.js +298 -15
  20. package/lib/stream-json-parser.js +4 -238
  21. package/package.json +27 -6
  22. package/scripts/setup-merge-queue.sh +170 -0
  23. package/scripts/validate-templates.js +100 -0
  24. package/src/agent/agent-config.js +140 -63
  25. package/src/agent/agent-context-builder.js +336 -165
  26. package/src/agent/agent-hook-executor.js +337 -67
  27. package/src/agent/agent-lifecycle.js +386 -287
  28. package/src/agent/agent-stuck-detector.js +7 -7
  29. package/src/agent/agent-task-executor.js +944 -683
  30. package/src/agent/output-extraction.js +217 -0
  31. package/src/agent/output-reformatter.js +175 -0
  32. package/src/agent/schema-utils.js +146 -0
  33. package/src/agent-wrapper.js +112 -31
  34. package/src/agents/git-pusher-template.js +285 -0
  35. package/src/claude-task-runner.js +145 -44
  36. package/src/config-router.js +13 -13
  37. package/src/config-validator.js +1049 -563
  38. package/src/input-helpers.js +65 -0
  39. package/src/isolation-manager.js +499 -320
  40. package/src/issue-providers/README.md +305 -0
  41. package/src/issue-providers/azure-devops-provider.js +273 -0
  42. package/src/issue-providers/base-provider.js +232 -0
  43. package/src/issue-providers/github-provider.js +179 -0
  44. package/src/issue-providers/gitlab-provider.js +241 -0
  45. package/src/issue-providers/index.js +196 -0
  46. package/src/issue-providers/jira-provider.js +239 -0
  47. package/src/ledger.js +50 -11
  48. package/src/lib/safe-exec.js +88 -0
  49. package/src/orchestrator.js +1348 -757
  50. package/src/preflight.js +306 -149
  51. package/src/process-metrics.js +98 -56
  52. package/src/providers/anthropic/cli-builder.js +73 -0
  53. package/src/providers/anthropic/index.js +204 -0
  54. package/src/providers/anthropic/models.js +23 -0
  55. package/src/providers/anthropic/output-parser.js +177 -0
  56. package/src/providers/base-provider.js +251 -0
  57. package/src/providers/capabilities.js +60 -0
  58. package/src/providers/google/cli-builder.js +55 -0
  59. package/src/providers/google/index.js +116 -0
  60. package/src/providers/google/models.js +24 -0
  61. package/src/providers/google/output-parser.js +101 -0
  62. package/src/providers/index.js +91 -0
  63. package/src/providers/openai/cli-builder.js +133 -0
  64. package/src/providers/openai/index.js +136 -0
  65. package/src/providers/openai/models.js +21 -0
  66. package/src/providers/openai/output-parser.js +143 -0
  67. package/src/providers/opencode/cli-builder.js +42 -0
  68. package/src/providers/opencode/index.js +103 -0
  69. package/src/providers/opencode/models.js +55 -0
  70. package/src/providers/opencode/output-parser.js +122 -0
  71. package/src/schemas/sub-cluster.js +68 -39
  72. package/src/status-footer.js +94 -48
  73. package/src/sub-cluster-wrapper.js +92 -36
  74. package/src/task-runner.js +8 -6
  75. package/src/template-resolver.js +12 -9
  76. package/src/tui/data-poller.js +123 -99
  77. package/src/tui/formatters.js +4 -3
  78. package/src/tui/keybindings.js +259 -318
  79. package/src/tui/layout.js +20 -3
  80. package/src/tui/renderer.js +11 -21
  81. package/task-lib/attachable-watcher.js +150 -111
  82. package/task-lib/claude-recovery.js +156 -0
  83. package/task-lib/commands/episodes.js +105 -0
  84. package/task-lib/commands/list.js +3 -3
  85. package/task-lib/commands/logs.js +231 -189
  86. package/task-lib/commands/resume.js +3 -2
  87. package/task-lib/commands/run.js +12 -3
  88. package/task-lib/commands/schedules.js +111 -61
  89. package/task-lib/config.js +0 -2
  90. package/task-lib/runner.js +128 -50
  91. package/task-lib/scheduler.js +3 -3
  92. package/task-lib/store.js +464 -152
  93. package/task-lib/tui/formatters.js +94 -45
  94. package/task-lib/tui/renderer.js +13 -3
  95. package/task-lib/tui.js +73 -32
  96. package/task-lib/watcher.js +157 -100
  97. package/src/agents/git-pusher-agent.json +0 -20
  98. package/src/github.js +0 -103
package/task-lib/store.js CHANGED
@@ -1,31 +1,81 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
- import { TASKS_DIR, TASKS_FILE, LOGS_DIR, SCHEDULES_FILE } from './config.js';
1
+ /**
2
+ * Task Store - SQLite-backed storage for tasks and schedules
3
+ *
4
+ * Uses WAL mode for concurrent access - no file locks needed.
5
+ * Multiple processes can read/write simultaneously without contention.
6
+ */
7
+
8
+ import { existsSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ import Database from 'better-sqlite3';
11
+ import { TASKS_DIR, LOGS_DIR } from './config.js';
3
12
  import { generateName } from './name-generator.js';
4
- import lockfile from 'proper-lockfile';
5
-
6
- // Lock options for sync API (no retries allowed)
7
- const LOCK_OPTIONS = {
8
- stale: 30000, // Consider lock stale after 30s
9
- };
10
-
11
- // Retry wrapper for sync lock acquisition
12
- function lockWithRetry(file, options, maxRetries = 100, delayMs = 100) {
13
- for (let i = 0; i < maxRetries; i++) {
14
- try {
15
- return lockfile.lockSync(file, options);
16
- } catch (err) {
17
- if (err.code === 'ELOCKED' && i < maxRetries - 1) {
18
- // File is locked, wait and retry
19
- const start = Date.now();
20
- while (Date.now() - start < delayMs) {
21
- // Busy wait (sync)
22
- }
23
- continue;
24
- }
25
- throw err;
26
- }
27
- }
28
- throw new Error(`Failed to acquire lock after ${maxRetries} retries`);
13
+
14
+ const DB_FILE = join(TASKS_DIR, 'store.db');
15
+
16
+ /** @type {Database.Database | null} */
17
+ let db = null;
18
+
19
+ /**
20
+ * Get or create the database connection
21
+ * @returns {Database.Database}
22
+ */
23
+ function getDb() {
24
+ if (db) return db;
25
+
26
+ ensureDirs();
27
+
28
+ db = new Database(DB_FILE, { timeout: 5000 });
29
+
30
+ // WAL mode for concurrent access - this is the key fix
31
+ db.pragma('journal_mode = WAL');
32
+ db.pragma('synchronous = NORMAL');
33
+
34
+ // Create tables
35
+ db.exec(`
36
+ CREATE TABLE IF NOT EXISTS tasks (
37
+ id TEXT PRIMARY KEY,
38
+ prompt TEXT,
39
+ full_prompt TEXT,
40
+ cwd TEXT,
41
+ status TEXT NOT NULL DEFAULT 'pending',
42
+ pid INTEGER,
43
+ session_id TEXT,
44
+ log_file TEXT,
45
+ created_at TEXT NOT NULL,
46
+ updated_at TEXT NOT NULL,
47
+ exit_code INTEGER,
48
+ error TEXT,
49
+ provider TEXT,
50
+ model TEXT,
51
+ schedule_id TEXT,
52
+ socket_path TEXT,
53
+ attachable INTEGER DEFAULT 0
54
+ );
55
+
56
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
57
+ CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at);
58
+
59
+ CREATE TABLE IF NOT EXISTS schedules (
60
+ id TEXT PRIMARY KEY,
61
+ cron TEXT NOT NULL,
62
+ prompt TEXT NOT NULL,
63
+ cwd TEXT,
64
+ model TEXT,
65
+ model_level TEXT,
66
+ reasoning_effort TEXT,
67
+ provider TEXT,
68
+ enabled INTEGER DEFAULT 1,
69
+ last_run TEXT,
70
+ next_run TEXT,
71
+ created_at TEXT NOT NULL,
72
+ updated_at TEXT NOT NULL
73
+ );
74
+
75
+ CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
76
+ `);
77
+
78
+ return db;
29
79
  }
30
80
 
31
81
  export function ensureDirs() {
@@ -33,99 +83,237 @@ export function ensureDirs() {
33
83
  if (!existsSync(LOGS_DIR)) mkdirSync(LOGS_DIR, { recursive: true });
34
84
  }
35
85
 
86
+ // ============================================================================
87
+ // Tasks
88
+ // ============================================================================
89
+
36
90
  /**
37
- * Read tasks.json (no locking - use for read-only operations)
91
+ * Convert DB row to task object (camelCase)
38
92
  */
39
- export function loadTasks() {
40
- ensureDirs();
41
- if (!existsSync(TASKS_FILE)) return {};
42
- const content = readFileSync(TASKS_FILE, 'utf-8');
43
- try {
44
- return JSON.parse(content);
45
- } catch (error) {
46
- throw new Error(
47
- `CRITICAL: tasks.json is corrupted and cannot be parsed. Error: ${error.message}. Content: ${content.slice(0, 200)}...`
48
- );
49
- }
93
+ function rowToTask(row) {
94
+ if (!row) return null;
95
+ return {
96
+ id: row.id,
97
+ prompt: row.prompt,
98
+ fullPrompt: row.full_prompt,
99
+ cwd: row.cwd,
100
+ status: row.status,
101
+ pid: row.pid,
102
+ sessionId: row.session_id,
103
+ logFile: row.log_file,
104
+ createdAt: row.created_at,
105
+ updatedAt: row.updated_at,
106
+ exitCode: row.exit_code,
107
+ error: row.error,
108
+ provider: row.provider,
109
+ model: row.model,
110
+ scheduleId: row.schedule_id,
111
+ socketPath: row.socket_path,
112
+ attachable: Boolean(row.attachable),
113
+ };
50
114
  }
51
115
 
52
116
  /**
53
- * Write tasks.json (no locking - internal use only)
117
+ * Load all tasks as object keyed by id
118
+ * @returns {Object.<string, Object>}
54
119
  */
55
- export function saveTasks(tasks) {
56
- ensureDirs();
57
- writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
120
+ export function loadTasks() {
121
+ const rows = getDb().prepare('SELECT * FROM tasks ORDER BY created_at DESC').all();
122
+ const tasks = {};
123
+ for (const row of rows) {
124
+ const task = rowToTask(row);
125
+ tasks[task.id] = task;
126
+ }
127
+ return tasks;
58
128
  }
59
129
 
60
130
  /**
61
- * Atomic read-modify-write with file locking
62
- * @param {Function} modifier - Function that receives tasks object and returns modified tasks
63
- * @returns {any} - Return value from modifier function
131
+ * Save all tasks (replaces entire store - for migration compatibility)
132
+ * @param {Object.<string, Object>} tasks
64
133
  */
65
- export function withTasksLock(modifier) {
66
- ensureDirs();
67
-
68
- // Create file if it doesn't exist (needed for locking)
69
- if (!existsSync(TASKS_FILE)) {
70
- writeFileSync(TASKS_FILE, '{}');
71
- }
134
+ export function saveTasks(tasks) {
135
+ const database = getDb();
136
+ const insert = database.prepare(`
137
+ INSERT OR REPLACE INTO tasks (
138
+ id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
139
+ created_at, updated_at, exit_code, error, provider, model,
140
+ schedule_id, socket_path, attachable
141
+ ) VALUES (
142
+ @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
143
+ @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
144
+ @scheduleId, @socketPath, @attachable
145
+ )
146
+ `);
72
147
 
73
- let release;
74
- try {
75
- // Acquire lock (blocks until available)
76
- release = lockWithRetry(TASKS_FILE, LOCK_OPTIONS);
77
-
78
- // Read current state
79
- const content = readFileSync(TASKS_FILE, 'utf-8');
80
- let tasks;
81
- try {
82
- tasks = JSON.parse(content);
83
- } catch (error) {
84
- throw new Error(`CRITICAL: tasks.json is corrupted. Error: ${error.message}`);
148
+ const insertMany = database.transaction((tasksObj) => {
149
+ // Clear existing
150
+ database.prepare('DELETE FROM tasks').run();
151
+ // Insert all
152
+ for (const task of Object.values(tasksObj)) {
153
+ insert.run({
154
+ id: task.id,
155
+ prompt: task.prompt || null,
156
+ fullPrompt: task.fullPrompt || null,
157
+ cwd: task.cwd || null,
158
+ status: task.status || 'pending',
159
+ pid: task.pid || null,
160
+ sessionId: task.sessionId || null,
161
+ logFile: task.logFile || null,
162
+ createdAt: task.createdAt || new Date().toISOString(),
163
+ updatedAt: task.updatedAt || new Date().toISOString(),
164
+ exitCode: task.exitCode ?? null,
165
+ error: task.error || null,
166
+ provider: task.provider || null,
167
+ model: task.model || null,
168
+ scheduleId: task.scheduleId || null,
169
+ socketPath: task.socketPath || null,
170
+ attachable: task.attachable ? 1 : 0,
171
+ });
85
172
  }
173
+ });
86
174
 
87
- // Apply modification
88
- const result = modifier(tasks);
89
-
90
- // Write back
91
- writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
175
+ insertMany(tasks);
176
+ }
92
177
 
93
- return result;
94
- } finally {
95
- if (release) {
96
- release();
97
- }
98
- }
178
+ /**
179
+ * For API compatibility - just runs the modifier synchronously
180
+ * SQLite WAL handles concurrency, no lock needed
181
+ * @param {Function} modifier
182
+ * @returns {any}
183
+ */
184
+ export function withTasksLock(modifier) {
185
+ const tasks = loadTasks();
186
+ const result = modifier(tasks);
187
+ saveTasks(tasks);
188
+ return result;
99
189
  }
100
190
 
191
+ /**
192
+ * Get a single task by id
193
+ * @param {string} id
194
+ * @returns {Object|null}
195
+ */
101
196
  export function getTask(id) {
102
- const tasks = loadTasks();
103
- return tasks[id];
197
+ const row = getDb().prepare('SELECT * FROM tasks WHERE id = ?').get(id);
198
+ return rowToTask(row);
104
199
  }
105
200
 
201
+ /**
202
+ * Update a task
203
+ * @param {string} id
204
+ * @param {Object} updates
205
+ * @returns {Object|null}
206
+ */
106
207
  export function updateTask(id, updates) {
107
- return withTasksLock((tasks) => {
108
- if (!tasks[id]) return null;
109
- tasks[id] = {
110
- ...tasks[id],
111
- ...updates,
112
- updatedAt: new Date().toISOString(),
113
- };
114
- return tasks[id];
115
- });
208
+ const existing = getTask(id);
209
+ if (!existing) return null;
210
+
211
+ const updated = {
212
+ ...existing,
213
+ ...updates,
214
+ updatedAt: new Date().toISOString(),
215
+ };
216
+
217
+ getDb()
218
+ .prepare(
219
+ `
220
+ UPDATE tasks SET
221
+ prompt = @prompt,
222
+ full_prompt = @fullPrompt,
223
+ cwd = @cwd,
224
+ status = @status,
225
+ pid = @pid,
226
+ session_id = @sessionId,
227
+ log_file = @logFile,
228
+ updated_at = @updatedAt,
229
+ exit_code = @exitCode,
230
+ error = @error,
231
+ provider = @provider,
232
+ model = @model,
233
+ schedule_id = @scheduleId,
234
+ socket_path = @socketPath,
235
+ attachable = @attachable
236
+ WHERE id = @id
237
+ `
238
+ )
239
+ .run({
240
+ id: updated.id,
241
+ prompt: updated.prompt || null,
242
+ fullPrompt: updated.fullPrompt || null,
243
+ cwd: updated.cwd || null,
244
+ status: updated.status || 'pending',
245
+ pid: updated.pid || null,
246
+ sessionId: updated.sessionId || null,
247
+ logFile: updated.logFile || null,
248
+ updatedAt: updated.updatedAt,
249
+ exitCode: updated.exitCode ?? null,
250
+ error: updated.error || null,
251
+ provider: updated.provider || null,
252
+ model: updated.model || null,
253
+ scheduleId: updated.scheduleId || null,
254
+ socketPath: updated.socketPath || null,
255
+ attachable: updated.attachable ? 1 : 0,
256
+ });
257
+
258
+ return updated;
116
259
  }
117
260
 
261
+ /**
262
+ * Add a new task
263
+ * @param {Object} task
264
+ * @returns {Object}
265
+ */
118
266
  export function addTask(task) {
119
- return withTasksLock((tasks) => {
120
- tasks[task.id] = task;
121
- return task;
122
- });
267
+ const now = new Date().toISOString();
268
+ const fullTask = {
269
+ ...task,
270
+ createdAt: task.createdAt || now,
271
+ updatedAt: task.updatedAt || now,
272
+ };
273
+
274
+ getDb()
275
+ .prepare(
276
+ `
277
+ INSERT INTO tasks (
278
+ id, prompt, full_prompt, cwd, status, pid, session_id, log_file,
279
+ created_at, updated_at, exit_code, error, provider, model,
280
+ schedule_id, socket_path, attachable
281
+ ) VALUES (
282
+ @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @logFile,
283
+ @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
284
+ @scheduleId, @socketPath, @attachable
285
+ )
286
+ `
287
+ )
288
+ .run({
289
+ id: fullTask.id,
290
+ prompt: fullTask.prompt || null,
291
+ fullPrompt: fullTask.fullPrompt || null,
292
+ cwd: fullTask.cwd || null,
293
+ status: fullTask.status || 'pending',
294
+ pid: fullTask.pid || null,
295
+ sessionId: fullTask.sessionId || null,
296
+ logFile: fullTask.logFile || null,
297
+ createdAt: fullTask.createdAt,
298
+ updatedAt: fullTask.updatedAt,
299
+ exitCode: fullTask.exitCode ?? null,
300
+ error: fullTask.error || null,
301
+ provider: fullTask.provider || null,
302
+ model: fullTask.model || null,
303
+ scheduleId: fullTask.scheduleId || null,
304
+ socketPath: fullTask.socketPath || null,
305
+ attachable: fullTask.attachable ? 1 : 0,
306
+ });
307
+
308
+ return fullTask;
123
309
  }
124
310
 
311
+ /**
312
+ * Remove a task
313
+ * @param {string} id
314
+ */
125
315
  export function removeTask(id) {
126
- withTasksLock((tasks) => {
127
- delete tasks[id];
128
- });
316
+ getDb().prepare('DELETE FROM tasks WHERE id = ?').run(id);
129
317
  }
130
318
 
131
319
  export function generateId() {
@@ -136,82 +324,206 @@ export function generateScheduleId() {
136
324
  return generateName('sched');
137
325
  }
138
326
 
139
- // Schedule management - same pattern with locking
140
-
141
- function withSchedulesLock(modifier) {
142
- ensureDirs();
327
+ // ============================================================================
328
+ // Schedules
329
+ // ============================================================================
143
330
 
144
- if (!existsSync(SCHEDULES_FILE)) {
145
- writeFileSync(SCHEDULES_FILE, '{}');
146
- }
147
-
148
- let release;
149
- try {
150
- release = lockWithRetry(SCHEDULES_FILE, LOCK_OPTIONS);
151
-
152
- const content = readFileSync(SCHEDULES_FILE, 'utf-8');
153
- let schedules;
154
- try {
155
- schedules = JSON.parse(content);
156
- } catch (error) {
157
- throw new Error(`CRITICAL: schedules.json is corrupted. Error: ${error.message}`);
158
- }
159
-
160
- const result = modifier(schedules);
161
- writeFileSync(SCHEDULES_FILE, JSON.stringify(schedules, null, 2));
162
-
163
- return result;
164
- } finally {
165
- if (release) {
166
- release();
167
- }
168
- }
331
+ /**
332
+ * Convert DB row to schedule object (camelCase)
333
+ */
334
+ function rowToSchedule(row) {
335
+ if (!row) return null;
336
+ return {
337
+ id: row.id,
338
+ cron: row.cron,
339
+ prompt: row.prompt,
340
+ cwd: row.cwd,
341
+ model: row.model,
342
+ modelLevel: row.model_level,
343
+ reasoningEffort: row.reasoning_effort,
344
+ provider: row.provider,
345
+ enabled: Boolean(row.enabled),
346
+ lastRun: row.last_run,
347
+ nextRun: row.next_run,
348
+ createdAt: row.created_at,
349
+ updatedAt: row.updated_at,
350
+ };
169
351
  }
170
352
 
353
+ /**
354
+ * Load all schedules as object keyed by id
355
+ * @returns {Object.<string, Object>}
356
+ */
171
357
  export function loadSchedules() {
172
- ensureDirs();
173
- if (!existsSync(SCHEDULES_FILE)) return {};
174
- const content = readFileSync(SCHEDULES_FILE, 'utf-8');
175
- try {
176
- return JSON.parse(content);
177
- } catch (error) {
178
- throw new Error(
179
- `CRITICAL: schedules.json is corrupted and cannot be parsed. Error: ${error.message}. Content: ${content.slice(0, 200)}...`
180
- );
358
+ const rows = getDb().prepare('SELECT * FROM schedules ORDER BY created_at DESC').all();
359
+ const schedules = {};
360
+ for (const row of rows) {
361
+ const schedule = rowToSchedule(row);
362
+ schedules[schedule.id] = schedule;
181
363
  }
364
+ return schedules;
182
365
  }
183
366
 
367
+ /**
368
+ * Save all schedules (replaces entire store)
369
+ * @param {Object.<string, Object>} schedules
370
+ */
184
371
  export function saveSchedules(schedules) {
185
- ensureDirs();
186
- writeFileSync(SCHEDULES_FILE, JSON.stringify(schedules, null, 2));
372
+ const database = getDb();
373
+ const insert = database.prepare(`
374
+ INSERT OR REPLACE INTO schedules (
375
+ id, cron, prompt, cwd, model, model_level, reasoning_effort,
376
+ provider, enabled, last_run, next_run, created_at, updated_at
377
+ ) VALUES (
378
+ @id, @cron, @prompt, @cwd, @model, @modelLevel, @reasoningEffort,
379
+ @provider, @enabled, @lastRun, @nextRun, @createdAt, @updatedAt
380
+ )
381
+ `);
382
+
383
+ const insertMany = database.transaction((schedulesObj) => {
384
+ database.prepare('DELETE FROM schedules').run();
385
+ for (const schedule of Object.values(schedulesObj)) {
386
+ insert.run({
387
+ id: schedule.id,
388
+ cron: schedule.cron,
389
+ prompt: schedule.prompt,
390
+ cwd: schedule.cwd || null,
391
+ model: schedule.model || null,
392
+ modelLevel: schedule.modelLevel || null,
393
+ reasoningEffort: schedule.reasoningEffort || null,
394
+ provider: schedule.provider || null,
395
+ enabled: schedule.enabled ? 1 : 0,
396
+ lastRun: schedule.lastRun || null,
397
+ nextRun: schedule.nextRun || null,
398
+ createdAt: schedule.createdAt || new Date().toISOString(),
399
+ updatedAt: schedule.updatedAt || new Date().toISOString(),
400
+ });
401
+ }
402
+ });
403
+
404
+ insertMany(schedules);
187
405
  }
188
406
 
407
+ /**
408
+ * Get a single schedule by id
409
+ * @param {string} id
410
+ * @returns {Object|null}
411
+ */
189
412
  export function getSchedule(id) {
190
- const schedules = loadSchedules();
191
- return schedules[id];
413
+ const row = getDb().prepare('SELECT * FROM schedules WHERE id = ?').get(id);
414
+ return rowToSchedule(row);
192
415
  }
193
416
 
417
+ /**
418
+ * Add a new schedule
419
+ * @param {Object} schedule
420
+ * @returns {Object}
421
+ */
194
422
  export function addSchedule(schedule) {
195
- return withSchedulesLock((schedules) => {
196
- schedules[schedule.id] = schedule;
197
- return schedule;
198
- });
423
+ const now = new Date().toISOString();
424
+ const fullSchedule = {
425
+ ...schedule,
426
+ createdAt: schedule.createdAt || now,
427
+ updatedAt: schedule.updatedAt || now,
428
+ };
429
+
430
+ getDb()
431
+ .prepare(
432
+ `
433
+ INSERT INTO schedules (
434
+ id, cron, prompt, cwd, model, model_level, reasoning_effort,
435
+ provider, enabled, last_run, next_run, created_at, updated_at
436
+ ) VALUES (
437
+ @id, @cron, @prompt, @cwd, @model, @modelLevel, @reasoningEffort,
438
+ @provider, @enabled, @lastRun, @nextRun, @createdAt, @updatedAt
439
+ )
440
+ `
441
+ )
442
+ .run({
443
+ id: fullSchedule.id,
444
+ cron: fullSchedule.cron,
445
+ prompt: fullSchedule.prompt,
446
+ cwd: fullSchedule.cwd || null,
447
+ model: fullSchedule.model || null,
448
+ modelLevel: fullSchedule.modelLevel || null,
449
+ reasoningEffort: fullSchedule.reasoningEffort || null,
450
+ provider: fullSchedule.provider || null,
451
+ enabled: fullSchedule.enabled !== false ? 1 : 0,
452
+ lastRun: fullSchedule.lastRun || null,
453
+ nextRun: fullSchedule.nextRun || null,
454
+ createdAt: fullSchedule.createdAt,
455
+ updatedAt: fullSchedule.updatedAt,
456
+ });
457
+
458
+ return fullSchedule;
199
459
  }
200
460
 
461
+ /**
462
+ * Update a schedule
463
+ * @param {string} id
464
+ * @param {Object} updates
465
+ * @returns {Object|null}
466
+ */
201
467
  export function updateSchedule(id, updates) {
202
- return withSchedulesLock((schedules) => {
203
- if (!schedules[id]) return null;
204
- schedules[id] = {
205
- ...schedules[id],
206
- ...updates,
207
- updatedAt: new Date().toISOString(),
208
- };
209
- return schedules[id];
210
- });
468
+ const existing = getSchedule(id);
469
+ if (!existing) return null;
470
+
471
+ const updated = {
472
+ ...existing,
473
+ ...updates,
474
+ updatedAt: new Date().toISOString(),
475
+ };
476
+
477
+ getDb()
478
+ .prepare(
479
+ `
480
+ UPDATE schedules SET
481
+ cron = @cron,
482
+ prompt = @prompt,
483
+ cwd = @cwd,
484
+ model = @model,
485
+ model_level = @modelLevel,
486
+ reasoning_effort = @reasoningEffort,
487
+ provider = @provider,
488
+ enabled = @enabled,
489
+ last_run = @lastRun,
490
+ next_run = @nextRun,
491
+ updated_at = @updatedAt
492
+ WHERE id = @id
493
+ `
494
+ )
495
+ .run({
496
+ id: updated.id,
497
+ cron: updated.cron,
498
+ prompt: updated.prompt,
499
+ cwd: updated.cwd || null,
500
+ model: updated.model || null,
501
+ modelLevel: updated.modelLevel || null,
502
+ reasoningEffort: updated.reasoningEffort || null,
503
+ provider: updated.provider || null,
504
+ enabled: updated.enabled ? 1 : 0,
505
+ lastRun: updated.lastRun || null,
506
+ nextRun: updated.nextRun || null,
507
+ updatedAt: updated.updatedAt,
508
+ });
509
+
510
+ return updated;
211
511
  }
212
512
 
513
+ /**
514
+ * Remove a schedule
515
+ * @param {string} id
516
+ */
213
517
  export function removeSchedule(id) {
214
- withSchedulesLock((schedules) => {
215
- delete schedules[id];
216
- });
518
+ getDb().prepare('DELETE FROM schedules WHERE id = ?').run(id);
519
+ }
520
+
521
+ /**
522
+ * Close the database connection (for cleanup)
523
+ */
524
+ export function closeDb() {
525
+ if (db) {
526
+ db.close();
527
+ db = null;
528
+ }
217
529
  }