0agent 1.0.28 → 1.0.29
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/bin/chat.js +81 -1
- package/dist/daemon.mjs +401 -2
- package/package.json +1 -1
package/bin/chat.js
CHANGED
|
@@ -189,6 +189,13 @@ function handleWsEvent(event) {
|
|
|
189
189
|
lineBuffer += event.token;
|
|
190
190
|
break;
|
|
191
191
|
}
|
|
192
|
+
case 'schedule.fired': {
|
|
193
|
+
// Show when a scheduled job fires — even if user is idle
|
|
194
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
195
|
+
process.stdout.write(`\n ${fmt(C.magenta, '⏰')} [${ts}] Scheduled: ${fmt(C.bold, event.job_name)} — ${event.task}\n`);
|
|
196
|
+
if (!streaming) rl.prompt(true);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
192
199
|
case 'session.completed': {
|
|
193
200
|
spinner.stop();
|
|
194
201
|
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
@@ -380,6 +387,78 @@ async function handleCommand(input) {
|
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
// /skills
|
|
390
|
+
// /schedule — cron-like job scheduler
|
|
391
|
+
case '/schedule': {
|
|
392
|
+
const schedArgs = parts.slice(1);
|
|
393
|
+
const subCmd = schedArgs[0]?.toLowerCase() ?? 'list';
|
|
394
|
+
|
|
395
|
+
if (subCmd === 'list' || schedArgs.length === 0) {
|
|
396
|
+
const res = await fetch(`${BASE_URL}/api/schedule`).catch(() => null);
|
|
397
|
+
const jobs = res?.ok ? await res.json().catch(() => []) : [];
|
|
398
|
+
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
399
|
+
console.log('\n No scheduled jobs. Add one:\n ' +
|
|
400
|
+
fmt(C.dim, '/schedule add "run /retro" every Friday at 5pm') + '\n');
|
|
401
|
+
} else {
|
|
402
|
+
console.log('\n Scheduled jobs:\n');
|
|
403
|
+
for (const j of jobs) {
|
|
404
|
+
const status = j.enabled ? fmt(C.green, '●') : fmt(C.dim, '○');
|
|
405
|
+
const next = j.next_run_human ?? 'unknown';
|
|
406
|
+
console.log(` ${status} ${fmt(C.bold, j.id)} ${j.name}`);
|
|
407
|
+
console.log(` ${fmt(C.dim, j.schedule_human + ' · next: ' + next)}`);
|
|
408
|
+
}
|
|
409
|
+
console.log();
|
|
410
|
+
}
|
|
411
|
+
} else if (subCmd === 'add') {
|
|
412
|
+
// /schedule add "<task>" <schedule...>
|
|
413
|
+
// Parse: extract quoted task, rest is schedule
|
|
414
|
+
const rest = parts.slice(2).join(' ');
|
|
415
|
+
const quoted = rest.match(/^"([^"]+)"\s+(.+)$/) || rest.match(/^'([^']+)'\s+(.+)$/);
|
|
416
|
+
if (!quoted) {
|
|
417
|
+
console.log(` ${fmt(C.dim, 'Usage: /schedule add "<task>" <schedule>')}`);
|
|
418
|
+
console.log(` ${fmt(C.dim, 'Examples:')}`);
|
|
419
|
+
console.log(` ${fmt(C.cyan, ' /schedule add "run /retro" every Friday at 5pm')}`);
|
|
420
|
+
console.log(` ${fmt(C.cyan, ' /schedule add "run /review" every day at 9am')}`);
|
|
421
|
+
console.log(` ${fmt(C.cyan, ' /schedule add "check the build" in 2 hours')}\n`);
|
|
422
|
+
} else {
|
|
423
|
+
const task = quoted[1];
|
|
424
|
+
const schedule = quoted[2];
|
|
425
|
+
const res = await fetch(`${BASE_URL}/api/schedule`, {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: { 'Content-Type': 'application/json' },
|
|
428
|
+
body: JSON.stringify({ task, schedule }),
|
|
429
|
+
}).catch(() => null);
|
|
430
|
+
const data = res?.ok ? await res.json().catch(() => null) : null;
|
|
431
|
+
if (data?.id) {
|
|
432
|
+
console.log(` ${fmt(C.green, '✓')} Scheduled: ${fmt(C.bold, data.name)}`);
|
|
433
|
+
console.log(` ${fmt(C.dim, data.schedule_human + ' · next: ' + data.next_run_human)}\n`);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(` ${fmt(C.red, '✗')} ${data?.error ?? 'Failed to create schedule'}\n`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} else if (subCmd === 'delete' || subCmd === 'remove') {
|
|
439
|
+
const id = schedArgs[1];
|
|
440
|
+
if (!id) { console.log(' Usage: /schedule delete <id>\n'); break; }
|
|
441
|
+
const res = await fetch(`${BASE_URL}/api/schedule/${id}`, { method: 'DELETE' }).catch(() => null);
|
|
442
|
+
const data = res?.ok ? await res.json().catch(() => null) : null;
|
|
443
|
+
console.log(data?.ok
|
|
444
|
+
? ` ${fmt(C.green, '✓')} Deleted ${id}\n`
|
|
445
|
+
: ` ${fmt(C.red, '✗')} ${data?.error ?? 'Not found'}\n`);
|
|
446
|
+
} else if (subCmd === 'pause') {
|
|
447
|
+
const id = schedArgs[1];
|
|
448
|
+
if (!id) { console.log(' Usage: /schedule pause <id>\n'); break; }
|
|
449
|
+
await fetch(`${BASE_URL}/api/schedule/${id}/pause`, { method: 'POST' });
|
|
450
|
+
console.log(` ${fmt(C.green, '✓')} Paused ${id}\n`);
|
|
451
|
+
} else if (subCmd === 'resume') {
|
|
452
|
+
const id = schedArgs[1];
|
|
453
|
+
if (!id) { console.log(' Usage: /schedule resume <id>\n'); break; }
|
|
454
|
+
await fetch(`${BASE_URL}/api/schedule/${id}/resume`, { method: 'POST' });
|
|
455
|
+
console.log(` ${fmt(C.green, '✓')} Resumed ${id}\n`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log(' Usage: /schedule list | add "<task>" <schedule> | delete <id> | pause <id> | resume <id>\n');
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
|
|
383
462
|
case '/skills': {
|
|
384
463
|
try {
|
|
385
464
|
const skills = await fetch(`${BASE_URL}/api/skills`).then(r => r.json());
|
|
@@ -442,6 +521,7 @@ const rl = createInterface({
|
|
|
442
521
|
historySize: 100,
|
|
443
522
|
completer: (line) => {
|
|
444
523
|
const commands = ['/model', '/key', '/status', '/skills', '/graph', '/clear', '/help',
|
|
524
|
+
'/schedule', '/schedule list', '/schedule add',
|
|
445
525
|
'/review', '/build', '/debug', '/qa', '/research', '/refactor', '/test-writer', '/retro'];
|
|
446
526
|
const hits = commands.filter(c => c.startsWith(line));
|
|
447
527
|
return [hits.length ? hits : commands, line];
|
|
@@ -569,7 +649,7 @@ rl.on('line', async (input) => {
|
|
|
569
649
|
const line = input.trim();
|
|
570
650
|
if (!line) { rl.prompt(); return; }
|
|
571
651
|
|
|
572
|
-
if (line.startsWith('/') || ['/model','/key','/status','/skills','/graph','/clear','/help'].some(c => line.startsWith(c))) {
|
|
652
|
+
if (line.startsWith('/') || ['/model','/key','/status','/skills','/graph','/clear','/help','/schedule'].some(c => line.startsWith(c))) {
|
|
573
653
|
await handleCommand(line);
|
|
574
654
|
rl.prompt();
|
|
575
655
|
} else {
|
package/dist/daemon.mjs
CHANGED
|
@@ -4748,7 +4748,7 @@ var SkillRegistry = class {
|
|
|
4748
4748
|
};
|
|
4749
4749
|
|
|
4750
4750
|
// packages/daemon/src/HTTPServer.ts
|
|
4751
|
-
import { Hono as
|
|
4751
|
+
import { Hono as Hono13 } from "hono";
|
|
4752
4752
|
import { serve } from "@hono/node-server";
|
|
4753
4753
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4754
4754
|
import { resolve as resolve7, dirname as dirname3 } from "node:path";
|
|
@@ -5137,6 +5137,398 @@ function codespaceRoutes(deps) {
|
|
|
5137
5137
|
return app;
|
|
5138
5138
|
}
|
|
5139
5139
|
|
|
5140
|
+
// packages/daemon/src/routes/schedule.ts
|
|
5141
|
+
import { Hono as Hono12 } from "hono";
|
|
5142
|
+
|
|
5143
|
+
// packages/daemon/src/SchedulerManager.ts
|
|
5144
|
+
var DAYS = {
|
|
5145
|
+
sunday: 0,
|
|
5146
|
+
sun: 0,
|
|
5147
|
+
monday: 1,
|
|
5148
|
+
mon: 1,
|
|
5149
|
+
tuesday: 2,
|
|
5150
|
+
tue: 2,
|
|
5151
|
+
tues: 2,
|
|
5152
|
+
wednesday: 3,
|
|
5153
|
+
wed: 3,
|
|
5154
|
+
thursday: 4,
|
|
5155
|
+
thu: 4,
|
|
5156
|
+
thur: 4,
|
|
5157
|
+
thurs: 4,
|
|
5158
|
+
friday: 5,
|
|
5159
|
+
fri: 5,
|
|
5160
|
+
saturday: 6,
|
|
5161
|
+
sat: 6
|
|
5162
|
+
};
|
|
5163
|
+
function parseTime(s) {
|
|
5164
|
+
if (!s) return { hour: 9, minute: 0 };
|
|
5165
|
+
const m = s.trim().match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i);
|
|
5166
|
+
if (!m) throw new Error(`Cannot parse time: "${s}". Use format like "9am", "5:30pm", "14:00"`);
|
|
5167
|
+
let hour = parseInt(m[1], 10);
|
|
5168
|
+
const minute = parseInt(m[2] ?? "0", 10);
|
|
5169
|
+
const ampm = m[3]?.toLowerCase();
|
|
5170
|
+
if (ampm === "pm" && hour !== 12) hour += 12;
|
|
5171
|
+
if (ampm === "am" && hour === 12) hour = 0;
|
|
5172
|
+
return { hour, minute };
|
|
5173
|
+
}
|
|
5174
|
+
function parseSchedule(text) {
|
|
5175
|
+
const t = text.trim().toLowerCase();
|
|
5176
|
+
const inMatch = t.match(/^in\s+(\d+)\s+(hour|hours|hr|hrs|minute|minutes|min|mins)$/);
|
|
5177
|
+
if (inMatch) {
|
|
5178
|
+
const n = parseInt(inMatch[1], 10);
|
|
5179
|
+
const isHours = inMatch[2].startsWith("h");
|
|
5180
|
+
const at = Date.now() + n * (isHours ? 36e5 : 6e4);
|
|
5181
|
+
return { spec: { type: "once", at }, human: text.trim() };
|
|
5182
|
+
}
|
|
5183
|
+
const tomorrowMatch = t.match(/^(?:tomorrow|tom)\s+at\s+(.+)$/);
|
|
5184
|
+
if (tomorrowMatch) {
|
|
5185
|
+
const { hour, minute } = parseTime(tomorrowMatch[1]);
|
|
5186
|
+
const d = /* @__PURE__ */ new Date();
|
|
5187
|
+
d.setDate(d.getDate() + 1);
|
|
5188
|
+
d.setHours(hour, minute, 0, 0);
|
|
5189
|
+
return { spec: { type: "once", at: d.getTime() }, human: text.trim() };
|
|
5190
|
+
}
|
|
5191
|
+
if (t === "every hour") return { spec: { type: "hourly", minute: 0 }, human: "every hour" };
|
|
5192
|
+
const everyMinsMatch = t.match(/^every\s+(\d+)\s+minutes?$/);
|
|
5193
|
+
if (everyMinsMatch) {
|
|
5194
|
+
const interval = parseInt(everyMinsMatch[1], 10);
|
|
5195
|
+
return { spec: { type: "interval_minutes", interval }, human: `every ${interval} minutes` };
|
|
5196
|
+
}
|
|
5197
|
+
const DEFAULT_TIMES = {
|
|
5198
|
+
morning: "9am",
|
|
5199
|
+
evening: "6pm",
|
|
5200
|
+
night: "10pm",
|
|
5201
|
+
noon: "12pm",
|
|
5202
|
+
midnight: "12am"
|
|
5203
|
+
};
|
|
5204
|
+
const dailyMatch = t.match(/^every\s+(day|daily|morning|evening|night|noon|midnight)\s*(?:at\s+(.+))?$/);
|
|
5205
|
+
if (dailyMatch) {
|
|
5206
|
+
const period = dailyMatch[1];
|
|
5207
|
+
const timeStr = dailyMatch[2] ?? DEFAULT_TIMES[period] ?? "9am";
|
|
5208
|
+
const { hour, minute } = parseTime(timeStr);
|
|
5209
|
+
const human = `every ${period}${dailyMatch[2] ? " at " + dailyMatch[2] : ""}`;
|
|
5210
|
+
return { spec: { type: "daily", hour, minute }, human };
|
|
5211
|
+
}
|
|
5212
|
+
const weeklyMatch = t.match(/^every\s+(\w+)\s*(?:at\s+(.+))?$/);
|
|
5213
|
+
if (weeklyMatch && DAYS[weeklyMatch[1]] !== void 0) {
|
|
5214
|
+
const day = DAYS[weeklyMatch[1]];
|
|
5215
|
+
const { hour, minute } = parseTime(weeklyMatch[2] ?? "9am");
|
|
5216
|
+
const human = `every ${weeklyMatch[1]}${weeklyMatch[2] ? " at " + weeklyMatch[2] : ""}`;
|
|
5217
|
+
return { spec: { type: "weekly", day, hour, minute }, human };
|
|
5218
|
+
}
|
|
5219
|
+
throw new Error(
|
|
5220
|
+
`Could not understand schedule: "${text}"
|
|
5221
|
+
Try: "every Friday at 5pm" \xB7 "every day at 9am" \xB7 "every morning" \xB7 "in 2 hours" \xB7 "every 30 minutes"`
|
|
5222
|
+
);
|
|
5223
|
+
}
|
|
5224
|
+
function scheduleToHuman(spec) {
|
|
5225
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
5226
|
+
const hhmm = (h, m) => {
|
|
5227
|
+
const ampm = h >= 12 ? "pm" : "am";
|
|
5228
|
+
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
5229
|
+
return m === 0 ? `${h12}${ampm}` : `${h12}:${pad(m)}${ampm}`;
|
|
5230
|
+
};
|
|
5231
|
+
const DAYS_REV = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
5232
|
+
switch (spec.type) {
|
|
5233
|
+
case "once":
|
|
5234
|
+
return `once at ${new Date(spec.at).toLocaleString()}`;
|
|
5235
|
+
case "hourly":
|
|
5236
|
+
return `every hour`;
|
|
5237
|
+
case "interval_minutes":
|
|
5238
|
+
return `every ${spec.interval} minutes`;
|
|
5239
|
+
case "daily":
|
|
5240
|
+
return `every day at ${hhmm(spec.hour, spec.minute)}`;
|
|
5241
|
+
case "weekly":
|
|
5242
|
+
return `every ${DAYS_REV[spec.day]} at ${hhmm(spec.hour, spec.minute)}`;
|
|
5243
|
+
case "monthly":
|
|
5244
|
+
return `monthly on the ${spec.date}th at ${hhmm(spec.hour, spec.minute)}`;
|
|
5245
|
+
default:
|
|
5246
|
+
return "unknown schedule";
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
function nextRunAt(spec, now = Date.now()) {
|
|
5250
|
+
const d = new Date(now);
|
|
5251
|
+
const next = new Date(now);
|
|
5252
|
+
switch (spec.type) {
|
|
5253
|
+
case "once":
|
|
5254
|
+
return spec.at;
|
|
5255
|
+
case "hourly": {
|
|
5256
|
+
next.setMinutes(spec.minute, 0, 0);
|
|
5257
|
+
if (next.getTime() <= now) next.setTime(next.getTime() + 36e5);
|
|
5258
|
+
return next.getTime();
|
|
5259
|
+
}
|
|
5260
|
+
case "interval_minutes": {
|
|
5261
|
+
const minsUntil = spec.interval - d.getMinutes() % spec.interval;
|
|
5262
|
+
return now + minsUntil * 6e4;
|
|
5263
|
+
}
|
|
5264
|
+
case "daily": {
|
|
5265
|
+
next.setHours(spec.hour, spec.minute, 0, 0);
|
|
5266
|
+
if (next.getTime() <= now) next.setDate(next.getDate() + 1);
|
|
5267
|
+
return next.getTime();
|
|
5268
|
+
}
|
|
5269
|
+
case "weekly": {
|
|
5270
|
+
const daysUntil = (spec.day - d.getDay() + 7) % 7 || 7;
|
|
5271
|
+
next.setDate(d.getDate() + daysUntil);
|
|
5272
|
+
next.setHours(spec.hour, spec.minute, 0, 0);
|
|
5273
|
+
if (next.getTime() <= now) next.setDate(next.getDate() + 7);
|
|
5274
|
+
return next.getTime();
|
|
5275
|
+
}
|
|
5276
|
+
case "monthly": {
|
|
5277
|
+
next.setDate(spec.date);
|
|
5278
|
+
next.setHours(spec.hour, spec.minute, 0, 0);
|
|
5279
|
+
if (next.getTime() <= now) next.setMonth(next.getMonth() + 1);
|
|
5280
|
+
return next.getTime();
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
var DDL = `
|
|
5285
|
+
CREATE TABLE IF NOT EXISTS scheduled_jobs (
|
|
5286
|
+
id TEXT PRIMARY KEY,
|
|
5287
|
+
name TEXT NOT NULL,
|
|
5288
|
+
task TEXT NOT NULL,
|
|
5289
|
+
skill TEXT,
|
|
5290
|
+
schedule_json TEXT NOT NULL,
|
|
5291
|
+
schedule_human TEXT NOT NULL,
|
|
5292
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
5293
|
+
last_run_at INTEGER,
|
|
5294
|
+
next_run_at INTEGER NOT NULL,
|
|
5295
|
+
run_count INTEGER NOT NULL DEFAULT 0,
|
|
5296
|
+
created_at INTEGER NOT NULL
|
|
5297
|
+
);
|
|
5298
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_next ON scheduled_jobs(next_run_at, enabled);
|
|
5299
|
+
`;
|
|
5300
|
+
var SchedulerStore = class {
|
|
5301
|
+
constructor(adapter) {
|
|
5302
|
+
this.adapter = adapter;
|
|
5303
|
+
}
|
|
5304
|
+
initialised = false;
|
|
5305
|
+
init() {
|
|
5306
|
+
if (this.initialised) return;
|
|
5307
|
+
const db = this.adapter.db;
|
|
5308
|
+
db.exec(DDL);
|
|
5309
|
+
this.initialised = true;
|
|
5310
|
+
}
|
|
5311
|
+
save(job) {
|
|
5312
|
+
this.init();
|
|
5313
|
+
const db = this.adapter.db;
|
|
5314
|
+
db.prepare(`
|
|
5315
|
+
INSERT OR REPLACE INTO scheduled_jobs
|
|
5316
|
+
(id, name, task, skill, schedule_json, schedule_human, enabled, last_run_at, next_run_at, run_count, created_at)
|
|
5317
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
5318
|
+
`).run(
|
|
5319
|
+
job.id,
|
|
5320
|
+
job.name,
|
|
5321
|
+
job.task,
|
|
5322
|
+
job.skill ?? null,
|
|
5323
|
+
JSON.stringify(job.schedule),
|
|
5324
|
+
job.schedule_human,
|
|
5325
|
+
job.enabled ? 1 : 0,
|
|
5326
|
+
job.last_run_at ?? null,
|
|
5327
|
+
job.next_run_at,
|
|
5328
|
+
job.run_count,
|
|
5329
|
+
job.created_at
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
5332
|
+
delete(id) {
|
|
5333
|
+
this.init();
|
|
5334
|
+
const db = this.adapter.db;
|
|
5335
|
+
db.prepare("DELETE FROM scheduled_jobs WHERE id = ?").run(id);
|
|
5336
|
+
}
|
|
5337
|
+
list() {
|
|
5338
|
+
this.init();
|
|
5339
|
+
const db = this.adapter.db;
|
|
5340
|
+
const rows = db.prepare("SELECT * FROM scheduled_jobs ORDER BY next_run_at ASC").all();
|
|
5341
|
+
return rows.map(this.rowToJob);
|
|
5342
|
+
}
|
|
5343
|
+
getDue(now) {
|
|
5344
|
+
this.init();
|
|
5345
|
+
const db = this.adapter.db;
|
|
5346
|
+
const rows = db.prepare(
|
|
5347
|
+
"SELECT * FROM scheduled_jobs WHERE enabled = 1 AND next_run_at <= ?"
|
|
5348
|
+
).all(now);
|
|
5349
|
+
return rows.map(this.rowToJob);
|
|
5350
|
+
}
|
|
5351
|
+
rowToJob(row) {
|
|
5352
|
+
return {
|
|
5353
|
+
id: row.id,
|
|
5354
|
+
name: row.name,
|
|
5355
|
+
task: row.task,
|
|
5356
|
+
skill: row.skill,
|
|
5357
|
+
schedule: JSON.parse(row.schedule_json),
|
|
5358
|
+
schedule_human: row.schedule_human,
|
|
5359
|
+
enabled: row.enabled === 1,
|
|
5360
|
+
last_run_at: row.last_run_at,
|
|
5361
|
+
next_run_at: row.next_run_at,
|
|
5362
|
+
run_count: row.run_count,
|
|
5363
|
+
created_at: row.created_at
|
|
5364
|
+
};
|
|
5365
|
+
}
|
|
5366
|
+
};
|
|
5367
|
+
var SchedulerManager = class {
|
|
5368
|
+
constructor(adapter, sessions, eventBus) {
|
|
5369
|
+
this.sessions = sessions;
|
|
5370
|
+
this.eventBus = eventBus;
|
|
5371
|
+
this.store = new SchedulerStore(adapter);
|
|
5372
|
+
this.store.init();
|
|
5373
|
+
}
|
|
5374
|
+
store;
|
|
5375
|
+
timer = null;
|
|
5376
|
+
start() {
|
|
5377
|
+
if (this.timer) return;
|
|
5378
|
+
this.timer = setInterval(() => this.tick(), 3e4);
|
|
5379
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
|
|
5380
|
+
this.timer.unref();
|
|
5381
|
+
}
|
|
5382
|
+
const init = setTimeout(() => this.tick(), 5e3);
|
|
5383
|
+
if (typeof init === "object" && "unref" in init) init.unref();
|
|
5384
|
+
}
|
|
5385
|
+
stop() {
|
|
5386
|
+
if (this.timer) {
|
|
5387
|
+
clearInterval(this.timer);
|
|
5388
|
+
this.timer = null;
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
/** Add a new scheduled job. */
|
|
5392
|
+
add(params) {
|
|
5393
|
+
const { spec, human } = parseSchedule(params.schedule);
|
|
5394
|
+
const now = Date.now();
|
|
5395
|
+
const job = {
|
|
5396
|
+
id: crypto.randomUUID().slice(0, 8),
|
|
5397
|
+
// short ID for easy reference
|
|
5398
|
+
name: params.name ?? params.task.slice(0, 40),
|
|
5399
|
+
task: params.task,
|
|
5400
|
+
skill: params.skill,
|
|
5401
|
+
schedule: spec,
|
|
5402
|
+
schedule_human: human,
|
|
5403
|
+
enabled: true,
|
|
5404
|
+
next_run_at: nextRunAt(spec, now),
|
|
5405
|
+
run_count: 0,
|
|
5406
|
+
created_at: now
|
|
5407
|
+
};
|
|
5408
|
+
this.store.save(job);
|
|
5409
|
+
return job;
|
|
5410
|
+
}
|
|
5411
|
+
/** Pause/resume a job. */
|
|
5412
|
+
setPaused(id, paused) {
|
|
5413
|
+
const jobs = this.store.list();
|
|
5414
|
+
const job = jobs.find((j) => j.id === id);
|
|
5415
|
+
if (!job) return false;
|
|
5416
|
+
job.enabled = !paused;
|
|
5417
|
+
this.store.save(job);
|
|
5418
|
+
return true;
|
|
5419
|
+
}
|
|
5420
|
+
/** Delete a job. */
|
|
5421
|
+
remove(id) {
|
|
5422
|
+
const jobs = this.store.list();
|
|
5423
|
+
const exists = jobs.some((j) => j.id === id);
|
|
5424
|
+
if (!exists) return false;
|
|
5425
|
+
this.store.delete(id);
|
|
5426
|
+
return true;
|
|
5427
|
+
}
|
|
5428
|
+
/** List all jobs. */
|
|
5429
|
+
list() {
|
|
5430
|
+
return this.store.list();
|
|
5431
|
+
}
|
|
5432
|
+
// ─── Tick ─────────────────────────────────────────────────────────────────
|
|
5433
|
+
async tick() {
|
|
5434
|
+
const now = Date.now();
|
|
5435
|
+
const due = this.store.getDue(now);
|
|
5436
|
+
for (const job of due) {
|
|
5437
|
+
if (job.last_run_at && now - job.last_run_at < 5e4) continue;
|
|
5438
|
+
await this.fire(job);
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
async fire(job) {
|
|
5442
|
+
job.last_run_at = Date.now();
|
|
5443
|
+
job.run_count++;
|
|
5444
|
+
if (job.schedule.type === "once") {
|
|
5445
|
+
job.enabled = false;
|
|
5446
|
+
} else {
|
|
5447
|
+
job.next_run_at = nextRunAt(job.schedule, Date.now() + 6e4);
|
|
5448
|
+
}
|
|
5449
|
+
this.store.save(job);
|
|
5450
|
+
this.eventBus.emit({
|
|
5451
|
+
type: "schedule.fired",
|
|
5452
|
+
job_id: job.id,
|
|
5453
|
+
job_name: job.name,
|
|
5454
|
+
task: job.task,
|
|
5455
|
+
run_count: job.run_count
|
|
5456
|
+
});
|
|
5457
|
+
try {
|
|
5458
|
+
const session = this.sessions.createSession({ task: job.task, skill: job.skill });
|
|
5459
|
+
this.sessions.runExistingSession(session.id, { task: job.task, skill: job.skill }).then(() => {
|
|
5460
|
+
this.eventBus.emit({ type: "schedule.completed", job_id: job.id, session_id: session.id });
|
|
5461
|
+
}).catch((err) => {
|
|
5462
|
+
this.eventBus.emit({ type: "schedule.error", job_id: job.id, error: String(err) });
|
|
5463
|
+
});
|
|
5464
|
+
} catch (err) {
|
|
5465
|
+
this.eventBus.emit({ type: "schedule.error", job_id: job.id, error: String(err) });
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
};
|
|
5469
|
+
|
|
5470
|
+
// packages/daemon/src/routes/schedule.ts
|
|
5471
|
+
function scheduleRoutes(deps) {
|
|
5472
|
+
const app = new Hono12();
|
|
5473
|
+
const getScheduler = (c) => {
|
|
5474
|
+
if (!deps.scheduler) {
|
|
5475
|
+
return { error: c.json({ error: "Scheduler not available" }, 503) };
|
|
5476
|
+
}
|
|
5477
|
+
return { scheduler: deps.scheduler };
|
|
5478
|
+
};
|
|
5479
|
+
app.get("/", (c) => {
|
|
5480
|
+
const { scheduler, error } = getScheduler(c);
|
|
5481
|
+
if (error) return error;
|
|
5482
|
+
const jobs = scheduler.list().map((j) => ({
|
|
5483
|
+
...j,
|
|
5484
|
+
schedule_human: j.schedule_human || scheduleToHuman(j.schedule),
|
|
5485
|
+
next_run_human: new Date(j.next_run_at).toLocaleString()
|
|
5486
|
+
}));
|
|
5487
|
+
return c.json(jobs);
|
|
5488
|
+
});
|
|
5489
|
+
app.post("/", async (c) => {
|
|
5490
|
+
const { scheduler, error } = getScheduler(c);
|
|
5491
|
+
if (error) return error;
|
|
5492
|
+
const body = await c.req.json();
|
|
5493
|
+
if (!body.task || !body.schedule) {
|
|
5494
|
+
return c.json({ error: "task and schedule are required" }, 400);
|
|
5495
|
+
}
|
|
5496
|
+
try {
|
|
5497
|
+
const job = scheduler.add({
|
|
5498
|
+
task: body.task,
|
|
5499
|
+
schedule: body.schedule,
|
|
5500
|
+
name: body.name,
|
|
5501
|
+
skill: body.skill
|
|
5502
|
+
});
|
|
5503
|
+
return c.json({
|
|
5504
|
+
...job,
|
|
5505
|
+
next_run_human: new Date(job.next_run_at).toLocaleString()
|
|
5506
|
+
}, 201);
|
|
5507
|
+
} catch (err) {
|
|
5508
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
|
|
5509
|
+
}
|
|
5510
|
+
});
|
|
5511
|
+
app.delete("/:id", (c) => {
|
|
5512
|
+
const { scheduler, error } = getScheduler(c);
|
|
5513
|
+
if (error) return error;
|
|
5514
|
+
const ok = scheduler.remove(c.req.param("id"));
|
|
5515
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "Job not found" }, 404);
|
|
5516
|
+
});
|
|
5517
|
+
app.post("/:id/pause", (c) => {
|
|
5518
|
+
const { scheduler, error } = getScheduler(c);
|
|
5519
|
+
if (error) return error;
|
|
5520
|
+
const ok = scheduler.setPaused(c.req.param("id"), true);
|
|
5521
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "Job not found" }, 404);
|
|
5522
|
+
});
|
|
5523
|
+
app.post("/:id/resume", (c) => {
|
|
5524
|
+
const { scheduler, error } = getScheduler(c);
|
|
5525
|
+
if (error) return error;
|
|
5526
|
+
const ok = scheduler.setPaused(c.req.param("id"), false);
|
|
5527
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "Job not found" }, 404);
|
|
5528
|
+
});
|
|
5529
|
+
return app;
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5140
5532
|
// packages/daemon/src/HTTPServer.ts
|
|
5141
5533
|
function findGraphHtml() {
|
|
5142
5534
|
const candidates = [
|
|
@@ -5162,7 +5554,7 @@ var HTTPServer = class {
|
|
|
5162
5554
|
deps;
|
|
5163
5555
|
constructor(deps) {
|
|
5164
5556
|
this.deps = deps;
|
|
5165
|
-
this.app = new
|
|
5557
|
+
this.app = new Hono13();
|
|
5166
5558
|
this.app.route("/api/health", healthRoutes({ getStatus: deps.getStatus }));
|
|
5167
5559
|
this.app.route("/api/sessions", sessionRoutes({ sessions: deps.sessions }));
|
|
5168
5560
|
this.app.route("/api/graph", graphRoutes({ graph: deps.graph }));
|
|
@@ -5173,6 +5565,7 @@ var HTTPServer = class {
|
|
|
5173
5565
|
this.app.route("/api/insights", insightsRoutes({ proactiveSurface: deps.proactiveSurface ?? null }));
|
|
5174
5566
|
this.app.route("/api/memory", memoryRoutes({ getSync: deps.getMemorySync ?? (() => null) }));
|
|
5175
5567
|
this.app.route("/api/llm", llmRoutes());
|
|
5568
|
+
this.app.route("/api/schedule", scheduleRoutes({ scheduler: deps.scheduler ?? null }));
|
|
5176
5569
|
this.app.route("/api/codespace", codespaceRoutes({
|
|
5177
5570
|
getManager: deps.getCodespaceManager ?? (() => null),
|
|
5178
5571
|
setup: deps.setupCodespace ?? (async () => ({ started: false, error: "Not configured" }))
|
|
@@ -6045,6 +6438,7 @@ var ZeroAgentDaemon = class {
|
|
|
6045
6438
|
memorySyncTimer = null;
|
|
6046
6439
|
proactiveSurfaceInstance = null;
|
|
6047
6440
|
codespaceManager = null;
|
|
6441
|
+
schedulerManager = null;
|
|
6048
6442
|
startedAt = 0;
|
|
6049
6443
|
pidFilePath;
|
|
6050
6444
|
constructor() {
|
|
@@ -6144,6 +6538,8 @@ var ZeroAgentDaemon = class {
|
|
|
6144
6538
|
proactiveSurface = new ProactiveSurface2(this.graph, this.eventBus, cwd);
|
|
6145
6539
|
} catch {
|
|
6146
6540
|
}
|
|
6541
|
+
this.schedulerManager = new SchedulerManager(this.adapter, this.sessionManager, this.eventBus);
|
|
6542
|
+
this.schedulerManager.start();
|
|
6147
6543
|
this.backgroundWorkers = new BackgroundWorkers({
|
|
6148
6544
|
graph: this.graph,
|
|
6149
6545
|
traceStore: this.traceStore,
|
|
@@ -6168,6 +6564,7 @@ var ZeroAgentDaemon = class {
|
|
|
6168
6564
|
getMemorySync: () => memSyncRef,
|
|
6169
6565
|
proactiveSurface,
|
|
6170
6566
|
getCodespaceManager: () => this.codespaceManager,
|
|
6567
|
+
scheduler: this.schedulerManager,
|
|
6171
6568
|
setupCodespace: async () => {
|
|
6172
6569
|
if (!this.codespaceManager) return { started: false, error: "GitHub memory not configured. Run: 0agent memory connect github" };
|
|
6173
6570
|
try {
|
|
@@ -6213,6 +6610,8 @@ var ZeroAgentDaemon = class {
|
|
|
6213
6610
|
this.memorySyncTimer = null;
|
|
6214
6611
|
}
|
|
6215
6612
|
this.githubMemorySync = null;
|
|
6613
|
+
this.schedulerManager?.stop();
|
|
6614
|
+
this.schedulerManager = null;
|
|
6216
6615
|
this.codespaceManager?.closeTunnel();
|
|
6217
6616
|
this.codespaceManager = null;
|
|
6218
6617
|
this.sessionManager = null;
|