@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.
- package/CHANGELOG.md +174 -189
- package/README.md +226 -195
- package/cli/commands/providers.js +149 -0
- package/cli/index.js +3145 -2366
- package/cli/lib/first-run.js +40 -3
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +24 -78
- package/cluster-templates/base-templates/full-workflow.json +99 -316
- package/cluster-templates/base-templates/single-worker.json +23 -15
- package/cluster-templates/base-templates/worker-validator.json +105 -36
- package/cluster-templates/conductor-bootstrap.json +9 -7
- package/lib/docker-config.js +14 -1
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +57 -0
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +298 -15
- package/lib/stream-json-parser.js +4 -238
- package/package.json +27 -6
- package/scripts/setup-merge-queue.sh +170 -0
- package/scripts/validate-templates.js +100 -0
- package/src/agent/agent-config.js +140 -63
- package/src/agent/agent-context-builder.js +336 -165
- package/src/agent/agent-hook-executor.js +337 -67
- package/src/agent/agent-lifecycle.js +386 -287
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +944 -683
- package/src/agent/output-extraction.js +217 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/schema-utils.js +146 -0
- package/src/agent-wrapper.js +112 -31
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +145 -44
- package/src/config-router.js +13 -13
- package/src/config-validator.js +1049 -563
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +499 -320
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +50 -11
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1348 -757
- package/src/preflight.js +306 -149
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +73 -0
- package/src/providers/anthropic/index.js +204 -0
- package/src/providers/anthropic/models.js +23 -0
- package/src/providers/anthropic/output-parser.js +177 -0
- package/src/providers/base-provider.js +251 -0
- package/src/providers/capabilities.js +60 -0
- package/src/providers/google/cli-builder.js +55 -0
- package/src/providers/google/index.js +116 -0
- package/src/providers/google/models.js +24 -0
- package/src/providers/google/output-parser.js +101 -0
- package/src/providers/index.js +91 -0
- package/src/providers/openai/cli-builder.js +133 -0
- package/src/providers/openai/index.js +136 -0
- package/src/providers/openai/models.js +21 -0
- package/src/providers/openai/output-parser.js +143 -0
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +92 -36
- package/src/task-runner.js +8 -6
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/layout.js +20 -3
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +150 -111
- package/task-lib/claude-recovery.js +156 -0
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +3 -3
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/resume.js +3 -2
- package/task-lib/commands/run.js +12 -3
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +128 -50
- package/task-lib/scheduler.js +3 -3
- package/task-lib/store.js +464 -152
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +157 -100
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -103
package/task-lib/store.js
CHANGED
|
@@ -1,31 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
*
|
|
91
|
+
* Convert DB row to task object (camelCase)
|
|
38
92
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
*
|
|
117
|
+
* Load all tasks as object keyed by id
|
|
118
|
+
* @returns {Object.<string, Object>}
|
|
54
119
|
*/
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
*
|
|
62
|
-
* @param {
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// Write back
|
|
91
|
-
writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
|
|
175
|
+
insertMany(tasks);
|
|
176
|
+
}
|
|
92
177
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 =
|
|
103
|
-
return
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
ensureDirs();
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// Schedules
|
|
329
|
+
// ============================================================================
|
|
143
330
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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 =
|
|
191
|
-
return
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
}
|