open-agents-ai 0.187.313 → 0.187.316
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/dist/index.js +543 -10
- package/dist/postinstall-daemon.cjs +91 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -251457,9 +251457,33 @@ function installCronJob(task, workingDir) {
|
|
|
251457
251457
|
const storeFile = resolve24(workingDir, ".oa", "scheduled", "tasks.json");
|
|
251458
251458
|
const taskEscaped = task.task.replace(/'/g, "'\\''");
|
|
251459
251459
|
const taskId = task.id;
|
|
251460
|
+
const lockDir = resolve24(workingDir, ".oa", "run");
|
|
251461
|
+
const lockPath = join37(lockDir, `${task.id}.lock`);
|
|
251460
251462
|
const wrapperCmd = [
|
|
251461
251463
|
`cd ${JSON.stringify(workingDir)}`,
|
|
251462
251464
|
`mkdir -p ${JSON.stringify(logDir)}`,
|
|
251465
|
+
`mkdir -p ${JSON.stringify(lockDir)}`,
|
|
251466
|
+
// Single-flight begin: try to acquire lock; if exists, check staleness
|
|
251467
|
+
`if mkdir ${JSON.stringify(lockPath)} 2>/dev/null; then`,
|
|
251468
|
+
` echo $$ > ${JSON.stringify(join37(lockPath, "pid"))}`,
|
|
251469
|
+
` trap 'rm -rf ${lockPath}' EXIT`,
|
|
251470
|
+
`else`,
|
|
251471
|
+
` if [ -f ${JSON.stringify(join37(lockPath, "pid"))} ]; then`,
|
|
251472
|
+
` oldpid=$(cat ${JSON.stringify(join37(lockPath, "pid"))} 2>/dev/null || echo)`,
|
|
251473
|
+
` if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then`,
|
|
251474
|
+
` echo "[oa-scheduler] ${taskId} already running as PID $oldpid; skipping" >> ${JSON.stringify(logFile)}`,
|
|
251475
|
+
` exit 0`,
|
|
251476
|
+
` else`,
|
|
251477
|
+
` # stale lock — remove and proceed`,
|
|
251478
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
251479
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join37(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
251480
|
+
` fi`,
|
|
251481
|
+
` else`,
|
|
251482
|
+
` # lock without pid file — assume stale`,
|
|
251483
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
251484
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join37(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
251485
|
+
` fi`,
|
|
251486
|
+
`fi`,
|
|
251463
251487
|
// Run the task and capture exit code
|
|
251464
251488
|
`${oaBin} '${taskEscaped}' >> ${JSON.stringify(logFile)} 2>&1; _oa_exit=$?`,
|
|
251465
251489
|
// Update store: increment runCount, set lastRun, handle oneShot/maxRuns
|
|
@@ -299357,22 +299381,74 @@ The session corrections MUST become hard rules in the SKILL.md Rules section.`;
|
|
|
299357
299381
|
};
|
|
299358
299382
|
if (sub === "menu") {
|
|
299359
299383
|
try {
|
|
299360
|
-
const r2 = await doFetch("/v1/scheduled");
|
|
299384
|
+
const r2 = await doFetch("/v1/scheduled/status");
|
|
299361
299385
|
const d2 = await r2.json();
|
|
299362
299386
|
const tasks = Array.isArray(d2.tasks) ? d2.tasks : [];
|
|
299363
299387
|
if (!tasks.length) {
|
|
299364
299388
|
renderInfo2("No scheduled tasks found.");
|
|
299365
299389
|
return "handled";
|
|
299366
299390
|
}
|
|
299367
|
-
const items = tasks.map((t2) =>
|
|
299368
|
-
|
|
299369
|
-
|
|
299370
|
-
|
|
299371
|
-
|
|
299391
|
+
const items = tasks.map((t2) => {
|
|
299392
|
+
const procInfo = t2.procs && t2.procs.length ? ` (${t2.procs.length} proc)` : "";
|
|
299393
|
+
const uptime2 = t2.procs && t2.procs[0]?.uptime_s ? ` • up ${Math.max(1, Math.round(t2.procs[0].uptime_s / 60))}m` : "";
|
|
299394
|
+
return {
|
|
299395
|
+
key: t2.id,
|
|
299396
|
+
label: `${t2.enabled ? "●" : "○"} ${t2.name || "(task)"} ${t2.schedule ? "[" + t2.schedule + "]" : ""}${procInfo}${uptime2}`,
|
|
299397
|
+
detail: `${t2.file}#${t2.index}`
|
|
299398
|
+
};
|
|
299399
|
+
});
|
|
299372
299400
|
items.push({ key: "__kill__", label: "Kill OA schedulers + active runs", detail: "Stop scheduler/nexus processes and terminate active OA runs" });
|
|
299373
299401
|
const result = await tuiSelect({
|
|
299374
299402
|
items,
|
|
299375
299403
|
title: "Scheduled Tasks",
|
|
299404
|
+
onAction: (item, action) => {
|
|
299405
|
+
if (item.key === "__kill__") return false;
|
|
299406
|
+
const task = tasks.find((t2) => t2.id === item.key);
|
|
299407
|
+
if (!task) return false;
|
|
299408
|
+
(async () => {
|
|
299409
|
+
const next = !task.enabled;
|
|
299410
|
+
const rr = await doFetch(`/v1/scheduled/${encodeURIComponent(task.id)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: next }) });
|
|
299411
|
+
if (rr.ok) {
|
|
299412
|
+
task.enabled = next;
|
|
299413
|
+
try {
|
|
299414
|
+
const rs = await doFetch("/v1/scheduled/status");
|
|
299415
|
+
const ds = await rs.json();
|
|
299416
|
+
const t2 = (Array.isArray(ds.tasks) ? ds.tasks : []).find((x) => x.id === task.id);
|
|
299417
|
+
if (t2) task.procs = t2.procs;
|
|
299418
|
+
} catch {
|
|
299419
|
+
}
|
|
299420
|
+
const pi = task.procs && task.procs.length ? ` (${task.procs.length} proc)` : "";
|
|
299421
|
+
const up = task.procs && task.procs[0]?.uptime_s ? ` • up ${Math.max(1, Math.round(task.procs[0].uptime_s / 60))}m` : "";
|
|
299422
|
+
item.label = `${next ? "●" : "○"} ${task.name || "(task)"} ${task.schedule ? "[" + task.schedule + "]" : ""}${pi}${up}`;
|
|
299423
|
+
renderInfo2(`${next ? "Enabled" : "Disabled"} ${task.id}`);
|
|
299424
|
+
} else {
|
|
299425
|
+
renderWarning2(`Failed to toggle ${task.id}`);
|
|
299426
|
+
}
|
|
299427
|
+
})();
|
|
299428
|
+
return true;
|
|
299429
|
+
},
|
|
299430
|
+
onCustomKey: (item, key, { done }) => {
|
|
299431
|
+
if (item.key === "__kill__") return false;
|
|
299432
|
+
if (key.toLowerCase() !== "k") return false;
|
|
299433
|
+
const task = tasks.find((t2) => t2.id === item.key);
|
|
299434
|
+
if (!task) return true;
|
|
299435
|
+
(async () => {
|
|
299436
|
+
const dir = (task.file || "").split("/").slice(0, -1).join("/") || task.file;
|
|
299437
|
+
const resp = await doFetch("/v1/scheduled/kill", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pattern: dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") }) });
|
|
299438
|
+
try {
|
|
299439
|
+
const j = await resp.json();
|
|
299440
|
+
const kb = Array.isArray(j.killed) ? j.killed.length : 0;
|
|
299441
|
+
const ka = Array.isArray(j.additionally) ? j.additionally.length : 0;
|
|
299442
|
+
renderInfo2(`Killed ${kb + ka} procs for selected task.`);
|
|
299443
|
+
const before = j.gpu_before?.[0];
|
|
299444
|
+
const after = j.gpu_after?.[0];
|
|
299445
|
+
if (before && after) renderInfo2(`GPU util: ${before.gpu_pct}% → ${after.gpu_pct}%`);
|
|
299446
|
+
} catch {
|
|
299447
|
+
}
|
|
299448
|
+
done();
|
|
299449
|
+
})();
|
|
299450
|
+
return true;
|
|
299451
|
+
},
|
|
299376
299452
|
onEnter: (item, { done }) => {
|
|
299377
299453
|
(async () => {
|
|
299378
299454
|
if (item.key === "__kill__") {
|
|
@@ -299424,6 +299500,35 @@ The session corrections MUST become hard rules in the SKILL.md Rules section.`;
|
|
|
299424
299500
|
}
|
|
299425
299501
|
return "handled";
|
|
299426
299502
|
}
|
|
299503
|
+
if (sub === "adopt" || sub === "reconcile") {
|
|
299504
|
+
try {
|
|
299505
|
+
renderInfo2("Reconciling cron → tasks.json (adopt missing entries)...");
|
|
299506
|
+
const r2 = await doFetch("/v1/scheduled/reconcile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apply: true }) });
|
|
299507
|
+
const d2 = await r2.json();
|
|
299508
|
+
const a2 = Array.isArray(d2.adopted) ? d2.adopted.length : 0;
|
|
299509
|
+
const m2 = Array.isArray(d2.mismatches) ? d2.mismatches.length : 0;
|
|
299510
|
+
const e2 = Array.isArray(d2.errors) ? d2.errors.length : 0;
|
|
299511
|
+
renderInfo2(`Adopted ${a2} entries${m2 ? `, ${m2} mismatches` : ""}${e2 ? `, ${e2} errors` : ""}.`);
|
|
299512
|
+
} catch (e2) {
|
|
299513
|
+
renderError2(e2?.message || String(e2));
|
|
299514
|
+
}
|
|
299515
|
+
return "handled";
|
|
299516
|
+
}
|
|
299517
|
+
if (sub === "fixup" || sub === "migrate") {
|
|
299518
|
+
const dry = tokens.includes("--dry-run") || tokens.includes("-n");
|
|
299519
|
+
try {
|
|
299520
|
+
const mode = sub === "fixup" ? "cron" : "migrate";
|
|
299521
|
+
renderInfo2(`${sub} ${dry ? "(dry-run)" : ""}...`);
|
|
299522
|
+
const r2 = await doFetch("/v1/scheduled/fixup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode, dryRun: dry }) });
|
|
299523
|
+
const d2 = await r2.json();
|
|
299524
|
+
const changed = Array.isArray(d2.changes) ? d2.changes.length : 0;
|
|
299525
|
+
const errs = Array.isArray(d2.errors) ? d2.errors.length : 0;
|
|
299526
|
+
renderInfo2(`${sub} complete: ${changed} changes${errs ? `, ${errs} errors` : ""}.`);
|
|
299527
|
+
} catch (e2) {
|
|
299528
|
+
renderError2(e2?.message || String(e2));
|
|
299529
|
+
}
|
|
299530
|
+
return "handled";
|
|
299531
|
+
}
|
|
299427
299532
|
if (sub === "list") {
|
|
299428
299533
|
try {
|
|
299429
299534
|
const r2 = await doFetch("/v1/scheduled");
|
|
@@ -320588,6 +320693,7 @@ body {
|
|
|
320588
320693
|
<div id="dashboard-health" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap"></div>
|
|
320589
320694
|
<div id="dashboard-daemons" style="margin-bottom:16px"></div>
|
|
320590
320695
|
<div id="dashboard-scheduled" style="margin-bottom:16px"></div>
|
|
320696
|
+
<div id="dashboard-services" style="margin-bottom:16px"></div>
|
|
320591
320697
|
<div id="dashboard-usage" style="margin-bottom:16px"></div>
|
|
320592
320698
|
<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Job History</h3>
|
|
320593
320699
|
<div id="jobs-list" style="font-size:0.78rem"></div>
|
|
@@ -321770,7 +321876,7 @@ async function loadDaemons() {
|
|
|
321770
321876
|
// Scheduled jobs panel
|
|
321771
321877
|
async function loadScheduled() {
|
|
321772
321878
|
try {
|
|
321773
|
-
const r = await fetch('/v1/scheduled', { headers: headers() });
|
|
321879
|
+
const r = await fetch('/v1/scheduled/status', { headers: headers() });
|
|
321774
321880
|
const d = await r.json();
|
|
321775
321881
|
const el = document.getElementById('dashboard-scheduled');
|
|
321776
321882
|
if (!el) return;
|
|
@@ -321785,10 +321891,13 @@ async function loadScheduled() {
|
|
|
321785
321891
|
? '<button onclick="toggleScheduled('' + t.id + '',false)" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>'
|
|
321786
321892
|
: '<button onclick="toggleScheduled('' + t.id + '',true)" style="background:#2a2a30;border:1px solid #2a3a2a;color:#4ec94e;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable</button>';
|
|
321787
321893
|
const color = enabled ? '#4ec94e' : '#5a2a2a';
|
|
321894
|
+
const procInfo = t.procs && t.procs.length ? (' (' + t.procs.length + ' proc)') : '';
|
|
321895
|
+
const up = t.procs && t.procs[0] && t.procs[0].uptime_s ? (' • up ' + Math.max(1, Math.round(t.procs[0].uptime_s/60)) + 'm') : '';
|
|
321896
|
+
const killBtn = '<button onclick="killScheduledTask('' + t.id + '')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">kill</button>';
|
|
321788
321897
|
const row = '<div style="background:#1e1e22;border-left:2px solid ' + color + ';padding:6px 10px;margin:4px 0;font-size:0.72rem">'
|
|
321789
|
-
+ '<div style="color:#b0b0b0">' + (t.name || '(task)') + ' <span style="color:#555">' + (t.schedule || '') + '</span
|
|
321898
|
+
+ '<div style="color:#b0b0b0">' + (t.name || '(task)') + ' <span style="color:#555">' + (t.schedule || '') + '</span>' + procInfo + up + '</div>'
|
|
321790
321899
|
+ '<div style="color:#555;font-size:0.6rem">' + t.file + '#' + t.index + '</div>'
|
|
321791
|
-
+ '<div style="margin-top:4px">' + btn + '</div>'
|
|
321900
|
+
+ '<div style="margin-top:4px;display:flex;gap:8px">' + btn + killBtn + '</div>'
|
|
321792
321901
|
+ '</div>';
|
|
321793
321902
|
return row;
|
|
321794
321903
|
}).join('');
|
|
@@ -321797,6 +321906,9 @@ async function loadScheduled() {
|
|
|
321797
321906
|
+ '<button onclick="disableAllScheduled()" title="Disable all scheduled tasks" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable all</button>'
|
|
321798
321907
|
+ '<button onclick="enableAllScheduled()" title="Enable all scheduled tasks" style="background:#2a2a30;border:1px solid #2a3a2a;color:#4ec94e;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">enable all</button>'
|
|
321799
321908
|
+ '<button onclick="killScheduled()" title="Kill OA scheduler processes" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">kill OA schedulers</button>'
|
|
321909
|
+
+ '<button onclick="adoptScheduled()" title="Adopt legacy cron jobs into tasks.json" style="background:#2a2a30;border:1px solid #2a2a5a;color:#6e7bd9;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">adopt</button>'
|
|
321910
|
+
+ '<button onclick="fixupScheduled()" title="Rewrite cron entries to canonical form" style="background:#2a2a30;border:1px solid #5a2a2a;color:#dba15f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">fixup</button>'
|
|
321911
|
+
+ '<button onclick="migrateScheduled()" title="Migrate cron → systemd user timers" style="background:#2a2a30;border:1px solid #5a2a2a;color:#a1db5f;padding:3px 8px;border-radius:3px;font-size:0.65rem;cursor:pointer">migrate</button>'
|
|
321800
321912
|
+ '</div>';
|
|
321801
321913
|
} catch {}
|
|
321802
321914
|
}
|
|
@@ -321864,6 +321976,110 @@ No remaining matched processes.';
|
|
|
321864
321976
|
} catch {}
|
|
321865
321977
|
}
|
|
321866
321978
|
|
|
321979
|
+
(window as any).adoptScheduled = async function() {
|
|
321980
|
+
try {
|
|
321981
|
+
const r = await fetch('/v1/scheduled/reconcile', { method:'POST', headers: headers(), body: JSON.stringify({ apply: true }) });
|
|
321982
|
+
const j = await r.json();
|
|
321983
|
+
const a = Array.isArray(j.adopted) ? j.adopted.length : 0;
|
|
321984
|
+
const m = Array.isArray(j.mismatches) ? j.mismatches.length : 0;
|
|
321985
|
+
const e = Array.isArray(j.errors) ? j.errors.length : 0;
|
|
321986
|
+
alert('Adopted ' + a + ' entries' + (m ? (', ' + m + ' mismatches') : '') + (e ? (', ' + e + ' errors') : ''));
|
|
321987
|
+
loadScheduled();
|
|
321988
|
+
} catch (e) { alert('Adopt failed: ' + (e && e.message || String(e))); }
|
|
321989
|
+
}
|
|
321990
|
+
|
|
321991
|
+
(window as any).fixupScheduled = async function() {
|
|
321992
|
+
try {
|
|
321993
|
+
if (!confirm('Rewrite OA cron entries to canonical launcher?')) return;
|
|
321994
|
+
const r = await fetch('/v1/scheduled/fixup', { method:'POST', headers: headers(), body: JSON.stringify({ mode: 'cron', dryRun: false }) });
|
|
321995
|
+
const j = await r.json();
|
|
321996
|
+
alert('Fixup: ' + (Array.isArray(j.changes)?j.changes.length:0) + ' changes, ' + (Array.isArray(j.errors)?j.errors.length:0) + ' errors');
|
|
321997
|
+
loadScheduled();
|
|
321998
|
+
} catch (e) { alert('Fixup failed: ' + (e && e.message || String(e))); }
|
|
321999
|
+
}
|
|
322000
|
+
|
|
322001
|
+
(window as any).migrateScheduled = async function() {
|
|
322002
|
+
try {
|
|
322003
|
+
if (!confirm('Migrate OA cron entries to systemd user timers?')) return;
|
|
322004
|
+
const r = await fetch('/v1/scheduled/fixup', { method:'POST', headers: headers(), body: JSON.stringify({ mode: 'migrate', dryRun: false }) });
|
|
322005
|
+
const j = await r.json();
|
|
322006
|
+
alert('Migrate: ' + (Array.isArray(j.changes)?j.changes.length:0) + ' changes, ' + (Array.isArray(j.errors)?j.errors.length:0) + ' errors');
|
|
322007
|
+
loadScheduled();
|
|
322008
|
+
} catch (e) { alert('Migrate failed: ' + (e && e.message || String(e))); }
|
|
322009
|
+
}
|
|
322010
|
+
|
|
322011
|
+
(window as any).killScheduledTask = async function(id) {
|
|
322012
|
+
try {
|
|
322013
|
+
// Fetch task to derive a pattern (directory of tasks.json)
|
|
322014
|
+
const r = await fetch('/v1/scheduled/status', { headers: headers() });
|
|
322015
|
+
const d = await r.json();
|
|
322016
|
+
const tasks = Array.isArray(d.tasks) ? d.tasks : [];
|
|
322017
|
+
const t = tasks.find(x => x.id === id);
|
|
322018
|
+
if (!t) { alert('Task not found'); return; }
|
|
322019
|
+
const dir = (t.file || '').split('/').slice(0, -1).join('/') || t.file;
|
|
322020
|
+
// Escape for regex without using a character class that includes brace/dollar combo (parser quirk)
|
|
322021
|
+
const safe = dir
|
|
322022
|
+
.replace(/\\/g, "\\\\")
|
|
322023
|
+
.replace(/[/g, "\\[")
|
|
322024
|
+
.replace(/]/g, "\\]")
|
|
322025
|
+
.replace(/{/g, "\\{")
|
|
322026
|
+
.replace(/}/g, "\\}")
|
|
322027
|
+
.replace(/(/g, "\\(")
|
|
322028
|
+
.replace(/)/g, "\\)")
|
|
322029
|
+
.replace(/*/g, "\\*")
|
|
322030
|
+
.replace(/+/g, "\\+")
|
|
322031
|
+
.replace(/?/g, "\\?")
|
|
322032
|
+
.replace(/^/g, "\\^")
|
|
322033
|
+
.replace(/$/g, "\\$")
|
|
322034
|
+
.replace(/|/g, "\\|")
|
|
322035
|
+
.replace(/./g, "\\.");
|
|
322036
|
+
const body = { pattern: safe };
|
|
322037
|
+
const resp = await fetch('/v1/scheduled/kill', { method:'POST', headers: headers(), body: JSON.stringify(body) });
|
|
322038
|
+
const j = await resp.json();
|
|
322039
|
+
const kb = Array.isArray(j.killed) ? j.killed.length : 0;
|
|
322040
|
+
const ka = Array.isArray(j.additionally) ? j.additionally.length : 0;
|
|
322041
|
+
const before = j.gpu_before && j.gpu_before[0] ? j.gpu_before[0] : null;
|
|
322042
|
+
const after = j.gpu_after && j.gpu_after[0] ? j.gpu_after[0] : null;
|
|
322043
|
+
const rem = Array.isArray(j.procs_after) ? j.procs_after.length : 0;
|
|
322044
|
+
let msg = 'Killed ' + (kb + ka) + ' processes for task.';
|
|
322045
|
+
if (before && after) msg += '
|
|
322046
|
+
GPU util: ' + before.gpu_pct + '% → ' + after.gpu_pct + '%';
|
|
322047
|
+
msg += rem > 0 ? ('
|
|
322048
|
+
Remaining matched processes: ' + rem) : '
|
|
322049
|
+
No remaining matched processes.';
|
|
322050
|
+
alert(msg);
|
|
322051
|
+
loadScheduled();
|
|
322052
|
+
} catch (e) { alert('Kill failed: ' + (e && e.message || String(e))); }
|
|
322053
|
+
}
|
|
322054
|
+
|
|
322055
|
+
async function loadServices() {
|
|
322056
|
+
try {
|
|
322057
|
+
const r = await fetch('/v1/services/systemd', { headers: headers() });
|
|
322058
|
+
const d = await r.json();
|
|
322059
|
+
const el = document.getElementById('dashboard-services');
|
|
322060
|
+
if (!el) return;
|
|
322061
|
+
const svcs = Array.isArray(d.services) ? d.services : [];
|
|
322062
|
+
if (!svcs.length) { el.innerHTML = ''; return; }
|
|
322063
|
+
const rows = svcs.map(s => {
|
|
322064
|
+
const stopBtn = '<button onclick="svcAction('' + s.name + '','stop')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">stop</button>';
|
|
322065
|
+
const disBtn = '<button onclick="svcAction('' + s.name + '','disable')" style="background:#2a2a30;border:1px solid #5a2a2a;color:#b25f5f;padding:2px 6px;border-radius:3px;font-size:0.65rem;cursor:pointer">disable</button>';
|
|
322066
|
+
return '<div style="background:#1e1e22;border-left:2px solid #3a3a42;padding:6px 10px;margin:4px 0;font-size:0.72rem">'
|
|
322067
|
+
+ '<div style="color:#b0b0b0">' + s.name + '</div>'
|
|
322068
|
+
+ '<div style="color:#555;font-size:0.6rem">enabled: ' + s.enabled + ' • active: ' + s.active + '</div>'
|
|
322069
|
+
+ '<div style="margin-top:4px;display:flex;gap:8px">' + stopBtn + disBtn + '</div>'
|
|
322070
|
+
+ '</div>';
|
|
322071
|
+
}).join('');
|
|
322072
|
+
el.innerHTML = '<h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Services (systemd --user)</h3>' + rows;
|
|
322073
|
+
} catch {}
|
|
322074
|
+
}
|
|
322075
|
+
|
|
322076
|
+
(window as any).svcAction = async function(name, action) {
|
|
322077
|
+
try {
|
|
322078
|
+
await fetch('/v1/services/systemd/' + encodeURIComponent(name), { method:'POST', headers: headers(), body: JSON.stringify({ action }) });
|
|
322079
|
+
loadServices();
|
|
322080
|
+
} catch (e) { alert('Service action failed: ' + (e && e.message || String(e))); }
|
|
322081
|
+
}
|
|
322082
|
+
|
|
321867
322083
|
// Agent task
|
|
321868
322084
|
let currentRunId = null;
|
|
321869
322085
|
async function loadProfiles() {
|
|
@@ -322994,6 +323210,7 @@ async function doUpdate() {
|
|
|
322994
323210
|
try { checkHealth(); } catch {}
|
|
322995
323211
|
try { pollMetrics(); } catch {}
|
|
322996
323212
|
try { loadScheduled(); } catch {}
|
|
323213
|
+
try { loadServices(); } catch {}
|
|
322997
323214
|
|
|
322998
323215
|
btn.textContent = 'updated v' + newVersion;
|
|
322999
323216
|
btn.style.background = '#1a3a1a';
|
|
@@ -323628,6 +323845,7 @@ restoreChatSession(); // WO-CHAT-RESUME — rehydrate from server state
|
|
|
323628
323845
|
setInterval(checkHealth, 30000);
|
|
323629
323846
|
setInterval(pollMetrics, 10000);
|
|
323630
323847
|
setInterval(loadScheduled, 15000);
|
|
323848
|
+
setInterval(loadServices, 30000);
|
|
323631
323849
|
setInterval(pollVersionBump, 5000);
|
|
323632
323850
|
input.focus();
|
|
323633
323851
|
</script>
|
|
@@ -327557,6 +327775,17 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
|
|
|
327557
327775
|
jsonResponse(res, 200, { tasks: listScheduledTasks() });
|
|
327558
327776
|
return;
|
|
327559
327777
|
}
|
|
327778
|
+
if (pathname === "/v1/scheduled/status" && method === "GET") {
|
|
327779
|
+
const tasks = listScheduledTasks();
|
|
327780
|
+
const enriched = tasks.map((t2) => {
|
|
327781
|
+
const dir = t2.file.split("/").slice(0, -1).join("/") || t2.file;
|
|
327782
|
+
const safe = dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
327783
|
+
const procs = listMatchingProcesses(safe);
|
|
327784
|
+
return { ...t2, procs };
|
|
327785
|
+
});
|
|
327786
|
+
jsonResponse(res, 200, { tasks: enriched });
|
|
327787
|
+
return;
|
|
327788
|
+
}
|
|
327560
327789
|
if (pathname?.startsWith("/v1/scheduled/") && method === "POST") {
|
|
327561
327790
|
const parts = pathname.split("/");
|
|
327562
327791
|
const id = parts[3] ?? "";
|
|
@@ -327619,6 +327848,37 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
|
|
|
327619
327848
|
});
|
|
327620
327849
|
return;
|
|
327621
327850
|
}
|
|
327851
|
+
if (pathname === "/v1/scheduled/reconcile" && (method === "GET" || method === "POST")) {
|
|
327852
|
+
const body = method === "POST" ? await parseJsonBody(req2) : {};
|
|
327853
|
+
const apply = Boolean(body?.apply);
|
|
327854
|
+
const result = reconcileScheduledTasks(apply);
|
|
327855
|
+
jsonResponse(res, 200, { applied: apply, ...result });
|
|
327856
|
+
return;
|
|
327857
|
+
}
|
|
327858
|
+
if (pathname === "/v1/scheduled/fixup" && method === "POST") {
|
|
327859
|
+
const body = await parseJsonBody(req2);
|
|
327860
|
+
const mode = String(body?.mode || "cron");
|
|
327861
|
+
const dryRun = Boolean(body?.dryRun);
|
|
327862
|
+
const result = fixupOrMigrateScheduled(mode, dryRun);
|
|
327863
|
+
jsonResponse(res, 200, { mode, dryRun, ...result });
|
|
327864
|
+
return;
|
|
327865
|
+
}
|
|
327866
|
+
if (pathname === "/v1/services/systemd" && method === "GET") {
|
|
327867
|
+
jsonResponse(res, 200, { services: listUserServices(/oa|open|agent|nexus|run-/i) });
|
|
327868
|
+
return;
|
|
327869
|
+
}
|
|
327870
|
+
if (pathname?.startsWith("/v1/services/systemd/") && method === "POST") {
|
|
327871
|
+
const unit = pathname.split("/")[4] || "";
|
|
327872
|
+
const body = await parseJsonBody(req2);
|
|
327873
|
+
const action = String(body?.action || "").toLowerCase();
|
|
327874
|
+
if (!unit || !action) {
|
|
327875
|
+
jsonResponse(res, 400, { error: "Missing unit or action" });
|
|
327876
|
+
return;
|
|
327877
|
+
}
|
|
327878
|
+
const ok2 = userServiceAction(unit, action);
|
|
327879
|
+
jsonResponse(res, ok2 ? 200 : 500, ok2 ? { unit, action, status: "ok" } : { error: "action failed", unit, action });
|
|
327880
|
+
return;
|
|
327881
|
+
}
|
|
327622
327882
|
if ((pathname === "/v1/chat" || pathname === "/api/chat") && method === "POST") {
|
|
327623
327883
|
if (!checkAuth(req2, res, "run")) {
|
|
327624
327884
|
status = 401;
|
|
@@ -328661,6 +328921,278 @@ function sampleGpuUtil() {
|
|
|
328661
328921
|
return null;
|
|
328662
328922
|
}
|
|
328663
328923
|
}
|
|
328924
|
+
function getCurrentCrontabLines() {
|
|
328925
|
+
try {
|
|
328926
|
+
const { execSync: es } = __require("node:child_process");
|
|
328927
|
+
return es("crontab -l 2>/dev/null", { encoding: "utf8", stdio: "pipe" }).split("\n");
|
|
328928
|
+
} catch {
|
|
328929
|
+
return [];
|
|
328930
|
+
}
|
|
328931
|
+
}
|
|
328932
|
+
function detectLegacyCron() {
|
|
328933
|
+
const lines = getCurrentCrontabLines();
|
|
328934
|
+
const out = [];
|
|
328935
|
+
for (const l2 of lines) {
|
|
328936
|
+
if (!l2.includes(CRON_MARKER2)) continue;
|
|
328937
|
+
const idMatch = l2.match(new RegExp(`${CRON_MARKER2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\S+)`));
|
|
328938
|
+
const id = idMatch?.[1] || "unknown";
|
|
328939
|
+
const parts = l2.trim().split(/\s+/);
|
|
328940
|
+
if (parts.length < 6) continue;
|
|
328941
|
+
const cron = parts.slice(0, 5).join(" ");
|
|
328942
|
+
let workingDir = null;
|
|
328943
|
+
const cdMatch = l2.match(/\bcd\s+(["'])([^"']+)\1/);
|
|
328944
|
+
if (cdMatch) workingDir = cdMatch[2];
|
|
328945
|
+
let task = null;
|
|
328946
|
+
try {
|
|
328947
|
+
const afterCd = l2.split(/\bcd\s+["'][^"']+["']/)[1] || l2;
|
|
328948
|
+
const oaIdx = afterCd.search(/\boa\b|open-agents-ai/);
|
|
328949
|
+
if (oaIdx >= 0) {
|
|
328950
|
+
const tail = afterCd.slice(oaIdx);
|
|
328951
|
+
const segMatch = tail.match(/['"][\s\S]*?>>/);
|
|
328952
|
+
if (segMatch) {
|
|
328953
|
+
let seg = segMatch[0];
|
|
328954
|
+
seg = seg.replace(/>>[\s\S]*/, "");
|
|
328955
|
+
seg = seg.replace(/'\\''/g, "\0");
|
|
328956
|
+
seg = seg.replace(/['"]/g, "");
|
|
328957
|
+
seg = seg.replace(/\u0000/g, "'");
|
|
328958
|
+
task = seg;
|
|
328959
|
+
}
|
|
328960
|
+
}
|
|
328961
|
+
} catch {
|
|
328962
|
+
}
|
|
328963
|
+
out.push({ id, cron, workingDir, task, line: l2 });
|
|
328964
|
+
}
|
|
328965
|
+
return out;
|
|
328966
|
+
}
|
|
328967
|
+
function reconcileScheduledTasks(apply) {
|
|
328968
|
+
const found = detectLegacyCron();
|
|
328969
|
+
const adopted = [];
|
|
328970
|
+
const mismatches = [];
|
|
328971
|
+
const errors = [];
|
|
328972
|
+
for (const f2 of found) {
|
|
328973
|
+
const wdir = f2.workingDir || process.cwd();
|
|
328974
|
+
const file = join99(wdir, ".oa", "scheduled", "tasks.json");
|
|
328975
|
+
try {
|
|
328976
|
+
let json = { tasks: [] };
|
|
328977
|
+
try {
|
|
328978
|
+
const raw = readFileSync65(file, "utf-8");
|
|
328979
|
+
json = JSON.parse(raw);
|
|
328980
|
+
} catch {
|
|
328981
|
+
}
|
|
328982
|
+
const arr = Array.isArray(json?.tasks) ? json.tasks : Array.isArray(json) ? json : [];
|
|
328983
|
+
const exists2 = arr.some((t2) => String(t2?.schedule || t2?.cron || "") === f2.cron && String(t2?.task || t2?.name || t2?.command || "") === (f2.task || ""));
|
|
328984
|
+
if (!exists2) {
|
|
328985
|
+
if (apply) {
|
|
328986
|
+
const entry = { task: f2.task || `legacy ${f2.id}`, schedule: f2.cron, enabled: true };
|
|
328987
|
+
arr.push(entry);
|
|
328988
|
+
const toWrite = Array.isArray(json?.tasks) ? { ...json, tasks: arr } : Array.isArray(json) ? arr : { tasks: arr };
|
|
328989
|
+
mkdirSync51(join99(wdir, ".oa", "scheduled"), { recursive: true });
|
|
328990
|
+
mkdirSync51(join99(wdir, ".oa", "scheduled", "logs"), { recursive: true });
|
|
328991
|
+
writeFileSync45(file, JSON.stringify(toWrite, null, 2));
|
|
328992
|
+
adopted.push({ file, index: arr.length - 1 });
|
|
328993
|
+
}
|
|
328994
|
+
} else {
|
|
328995
|
+
const idx = arr.findIndex((t2) => String(t2?.schedule || t2?.cron || "") === f2.cron && String(t2?.task || t2?.name || t2?.command || "") === (f2.task || ""));
|
|
328996
|
+
if (idx >= 0) {
|
|
328997
|
+
const en = typeof arr[idx].enabled === "boolean" ? arr[idx].enabled : true;
|
|
328998
|
+
if (!en) mismatches.push({ file, id: f2.id, reason: "cron present but task disabled in tasks.json" });
|
|
328999
|
+
}
|
|
329000
|
+
}
|
|
329001
|
+
} catch (e2) {
|
|
329002
|
+
errors.push({ line: f2.line, error: e2?.message || String(e2) });
|
|
329003
|
+
}
|
|
329004
|
+
}
|
|
329005
|
+
return { found: found.map(({ id, cron, workingDir, task }) => ({ id, cron, workingDir, task })), adopted, mismatches, errors };
|
|
329006
|
+
}
|
|
329007
|
+
function findOaBinary4() {
|
|
329008
|
+
try {
|
|
329009
|
+
const { execSync: es } = __require("node:child_process");
|
|
329010
|
+
for (const cmd of ["oa", "open-agents"]) {
|
|
329011
|
+
try {
|
|
329012
|
+
const p2 = es(`which ${cmd} 2>/dev/null`, { encoding: "utf8", stdio: "pipe" }).trim();
|
|
329013
|
+
if (p2) return p2;
|
|
329014
|
+
} catch {
|
|
329015
|
+
}
|
|
329016
|
+
}
|
|
329017
|
+
} catch {
|
|
329018
|
+
}
|
|
329019
|
+
return "npx open-agents-ai";
|
|
329020
|
+
}
|
|
329021
|
+
function getCrontabLines() {
|
|
329022
|
+
return getCurrentCrontabLines();
|
|
329023
|
+
}
|
|
329024
|
+
function writeCrontabLines(lines) {
|
|
329025
|
+
try {
|
|
329026
|
+
const { spawnSync: spawnSync6 } = __require("node:child_process");
|
|
329027
|
+
const result = spawnSync6("crontab", ["-"], { input: lines.join("\n") + "\n", encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
329028
|
+
if (result.status !== 0) throw new Error(`crontab exited ${result.status}: ${String(result.stderr || "").trim()}`);
|
|
329029
|
+
} catch (e2) {
|
|
329030
|
+
throw new Error(`writeCrontab failed: ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
329031
|
+
}
|
|
329032
|
+
}
|
|
329033
|
+
function canonicalCronLine(rec) {
|
|
329034
|
+
const oaBin = findOaBinary4();
|
|
329035
|
+
const logDir = join99(rec.workingDir, ".oa", "scheduled", "logs");
|
|
329036
|
+
const logFile = join99(logDir, `${rec.id}.log`);
|
|
329037
|
+
const storeFile = join99(rec.workingDir, ".oa", "scheduled", "tasks.json");
|
|
329038
|
+
const taskEsc = rec.task.replace(/'/g, "'\\''");
|
|
329039
|
+
const lockDir = join99(rec.workingDir, ".oa", "run");
|
|
329040
|
+
const lockPath = join99(lockDir, `${rec.id}.lock`);
|
|
329041
|
+
const wrapper = [
|
|
329042
|
+
`cd ${JSON.stringify(rec.workingDir)}`,
|
|
329043
|
+
`mkdir -p ${JSON.stringify(logDir)}`,
|
|
329044
|
+
`mkdir -p ${JSON.stringify(lockDir)}`,
|
|
329045
|
+
`if mkdir ${JSON.stringify(lockPath)} 2>/dev/null; then`,
|
|
329046
|
+
` echo $$ > ${JSON.stringify(join99(lockPath, "pid"))}`,
|
|
329047
|
+
` trap 'rm -rf ${lockPath}' EXIT`,
|
|
329048
|
+
`else`,
|
|
329049
|
+
` if [ -f ${JSON.stringify(join99(lockPath, "pid"))} ]; then`,
|
|
329050
|
+
` oldpid=$(cat ${JSON.stringify(join99(lockPath, "pid"))} 2>/dev/null || echo)`,
|
|
329051
|
+
` if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then`,
|
|
329052
|
+
` echo "[oa-scheduler] ${rec.id} already running as PID $oldpid; skipping" >> ${JSON.stringify(logFile)}`,
|
|
329053
|
+
` exit 0`,
|
|
329054
|
+
` else`,
|
|
329055
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
329056
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join99(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
329057
|
+
` fi`,
|
|
329058
|
+
` else`,
|
|
329059
|
+
` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
|
|
329060
|
+
` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join99(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
|
|
329061
|
+
` fi`,
|
|
329062
|
+
`fi`,
|
|
329063
|
+
`${oaBin} '${taskEsc}' >> ${JSON.stringify(logFile)} 2>&1; _oa_exit=$?`,
|
|
329064
|
+
`node -e "const fs=require('fs'),p=${JSON.stringify(storeFile)};try{const s=JSON.parse(fs.readFileSync(p,'utf8'));const t=s.tasks.find(x=>x.id==='${rec.id}');if(t){t.runCount=(t.runCount||0)+1;t.lastRun=new Date().toISOString();if(t.oneShot)t.enabled=false;if(t.maxRuns&&t.runCount>=t.maxRuns)t.enabled=false;s.updatedAt=new Date().toISOString();fs.writeFileSync(p,JSON.stringify(s,null,2));}}catch(e){}" 2>/dev/null`
|
|
329065
|
+
].join("; ");
|
|
329066
|
+
return `${rec.cron} ${wrapper} ${CRON_MARKER2}${rec.id}`;
|
|
329067
|
+
}
|
|
329068
|
+
function fixupOrMigrateScheduled(mode, dryRun) {
|
|
329069
|
+
const found = detectLegacyCron();
|
|
329070
|
+
const changes = [];
|
|
329071
|
+
const errors = [];
|
|
329072
|
+
if (mode === "cron") {
|
|
329073
|
+
try {
|
|
329074
|
+
const lines = getCrontabLines();
|
|
329075
|
+
let next = lines.filter((l2) => !l2.includes(CRON_MARKER2));
|
|
329076
|
+
for (const f2 of found) {
|
|
329077
|
+
if (!f2.workingDir || !f2.task) continue;
|
|
329078
|
+
const rec = { id: f2.id, cron: f2.cron, workingDir: f2.workingDir, task: f2.task };
|
|
329079
|
+
const newline = canonicalCronLine(rec);
|
|
329080
|
+
next.push(newline);
|
|
329081
|
+
changes.push({ id: f2.id, action: dryRun ? "would-rewrite" : "rewrite", detail: "cron line canonicalized" });
|
|
329082
|
+
}
|
|
329083
|
+
if (!dryRun) writeCrontabLines(next);
|
|
329084
|
+
} catch (e2) {
|
|
329085
|
+
errors.push({ error: e2?.message || String(e2) });
|
|
329086
|
+
}
|
|
329087
|
+
} else if (mode === "migrate" || mode === "systemd") {
|
|
329088
|
+
for (const f2 of found) {
|
|
329089
|
+
try {
|
|
329090
|
+
if (!f2.workingDir || !f2.task) continue;
|
|
329091
|
+
const unitBase = `oa-${f2.id}`;
|
|
329092
|
+
const unitDir = join99(homedir38(), ".config", "systemd", "user");
|
|
329093
|
+
const svc = join99(unitDir, `${unitBase}.service`);
|
|
329094
|
+
const tim = join99(unitDir, `${unitBase}.timer`);
|
|
329095
|
+
const oaBin = findOaBinary4();
|
|
329096
|
+
const rec = { id: f2.id, cron: f2.cron, workingDir: f2.workingDir, task: f2.task };
|
|
329097
|
+
const cmd = canonicalCronLine(rec).split(" ").slice(5).join(" ");
|
|
329098
|
+
const execCmd = `/bin/sh -lc ${JSON.stringify(cmd.replace(/\s+${CRON_MARKER}.+$/, "").trim())}`;
|
|
329099
|
+
const svcText = `[Unit]
|
|
329100
|
+
Description=Open Agents Scheduled Task ${f2.id}
|
|
329101
|
+
After=default.target
|
|
329102
|
+
|
|
329103
|
+
[Service]
|
|
329104
|
+
Type=oneshot
|
|
329105
|
+
ExecStart=${execCmd}
|
|
329106
|
+
WorkingDirectory=${f2.workingDir}
|
|
329107
|
+
Environment=OA_DAEMON=1
|
|
329108
|
+
|
|
329109
|
+
[Install]
|
|
329110
|
+
WantedBy=default.target
|
|
329111
|
+
`;
|
|
329112
|
+
const onCal = f2.cron.trim() === "* * * * *" ? "*:*:00" : f2.cron.trim().startsWith("*/") ? `*:0/${f2.cron.trim().split(/\s+/)[0].slice(2)}:00` : "*-*-* *:*:00";
|
|
329113
|
+
const timText = `[Unit]
|
|
329114
|
+
Description=Timer for ${unitBase}
|
|
329115
|
+
|
|
329116
|
+
[Timer]
|
|
329117
|
+
OnCalendar=${onCal}
|
|
329118
|
+
Persistent=true
|
|
329119
|
+
|
|
329120
|
+
[Install]
|
|
329121
|
+
WantedBy=timers.target
|
|
329122
|
+
`;
|
|
329123
|
+
if (!dryRun) {
|
|
329124
|
+
mkdirSync51(unitDir, { recursive: true });
|
|
329125
|
+
writeFileSync45(svc, svcText);
|
|
329126
|
+
writeFileSync45(tim, timText);
|
|
329127
|
+
try {
|
|
329128
|
+
const { execSync: es } = __require("node:child_process");
|
|
329129
|
+
es("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
329130
|
+
es(`systemctl --user enable --now ${unitBase}.timer`, { stdio: "pipe" });
|
|
329131
|
+
} catch {
|
|
329132
|
+
}
|
|
329133
|
+
}
|
|
329134
|
+
changes.push({ id: f2.id, action: dryRun ? "would-create-timer" : "create-timer", detail: `${unitBase}.timer` });
|
|
329135
|
+
} catch (e2) {
|
|
329136
|
+
errors.push({ id: f2.id, error: e2?.message || String(e2) });
|
|
329137
|
+
}
|
|
329138
|
+
}
|
|
329139
|
+
if (mode === "migrate") {
|
|
329140
|
+
try {
|
|
329141
|
+
const lines = getCrontabLines();
|
|
329142
|
+
const next = lines.filter((l2) => !l2.includes(CRON_MARKER2));
|
|
329143
|
+
if (!dryRun) writeCrontabLines(next);
|
|
329144
|
+
changes.push({ id: "*", action: dryRun ? "would-remove-cron" : "remove-cron", detail: "removed all OA cron entries" });
|
|
329145
|
+
} catch (e2) {
|
|
329146
|
+
errors.push({ error: e2?.message || String(e2) });
|
|
329147
|
+
}
|
|
329148
|
+
}
|
|
329149
|
+
}
|
|
329150
|
+
return { changes, errors };
|
|
329151
|
+
}
|
|
329152
|
+
function listUserServices(pattern) {
|
|
329153
|
+
const out = [];
|
|
329154
|
+
try {
|
|
329155
|
+
const { execSync: es } = __require("node:child_process");
|
|
329156
|
+
const files = es("systemctl --user list-unit-files --type=service --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
|
|
329157
|
+
const enabledMap = /* @__PURE__ */ new Map();
|
|
329158
|
+
for (const line of files) {
|
|
329159
|
+
const m2 = line.trim().match(/^(\S+)\s+(\S+)/);
|
|
329160
|
+
if (!m2) continue;
|
|
329161
|
+
const name11 = m2[1], state = m2[2];
|
|
329162
|
+
if (pattern.test(name11)) enabledMap.set(name11, state);
|
|
329163
|
+
}
|
|
329164
|
+
const units = es("systemctl --user list-units --type=service --all --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
|
|
329165
|
+
const activeMap = /* @__PURE__ */ new Map();
|
|
329166
|
+
for (const line of units) {
|
|
329167
|
+
const m2 = line.trim().match(/^(\S+)\s+\S+\s+\S+\s+(\S+)/);
|
|
329168
|
+
if (!m2) continue;
|
|
329169
|
+
const name11 = m2[1], state = m2[2];
|
|
329170
|
+
if (pattern.test(name11)) activeMap.set(name11, state);
|
|
329171
|
+
}
|
|
329172
|
+
for (const [name11, en] of enabledMap.entries()) {
|
|
329173
|
+
out.push({ name: name11, enabled: en, active: activeMap.get(name11) || "inactive" });
|
|
329174
|
+
}
|
|
329175
|
+
} catch {
|
|
329176
|
+
}
|
|
329177
|
+
return out;
|
|
329178
|
+
}
|
|
329179
|
+
function userServiceAction(unit, action) {
|
|
329180
|
+
try {
|
|
329181
|
+
const { execSync: es } = __require("node:child_process");
|
|
329182
|
+
if (action === "stop") {
|
|
329183
|
+
es(`systemctl --user stop ${unit}`, { stdio: "pipe" });
|
|
329184
|
+
return true;
|
|
329185
|
+
} else if (action === "disable") {
|
|
329186
|
+
es(`systemctl --user disable ${unit}`, { stdio: "pipe" });
|
|
329187
|
+
return true;
|
|
329188
|
+
} else if (action === "restart") {
|
|
329189
|
+
es(`systemctl --user restart ${unit}`, { stdio: "pipe" });
|
|
329190
|
+
return true;
|
|
329191
|
+
}
|
|
329192
|
+
} catch {
|
|
329193
|
+
}
|
|
329194
|
+
return false;
|
|
329195
|
+
}
|
|
328664
329196
|
function startApiServer(options2 = {}) {
|
|
328665
329197
|
if (options2.quiet) setQuiet(true);
|
|
328666
329198
|
const log22 = options2.quiet ? (_msg) => {
|
|
@@ -329208,7 +329740,7 @@ async function apiServeCommand(opts, config) {
|
|
|
329208
329740
|
server.on("close", resolve40);
|
|
329209
329741
|
});
|
|
329210
329742
|
}
|
|
329211
|
-
var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses, perKeyUsage;
|
|
329743
|
+
var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses, perKeyUsage, CRON_MARKER2;
|
|
329212
329744
|
var init_serve = __esm({
|
|
329213
329745
|
"packages/cli/src/api/serve.ts"() {
|
|
329214
329746
|
"use strict";
|
|
@@ -329245,6 +329777,7 @@ var init_serve = __esm({
|
|
|
329245
329777
|
_corsLocalOnly = _corsOrigins.length === 0;
|
|
329246
329778
|
runningProcesses = /* @__PURE__ */ new Map();
|
|
329247
329779
|
perKeyUsage = /* @__PURE__ */ new Map();
|
|
329780
|
+
CRON_MARKER2 = "# OPEN-AGENTS-SCHEDULED:";
|
|
329248
329781
|
}
|
|
329249
329782
|
});
|
|
329250
329783
|
|
|
@@ -1,4 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Postinstall hook
|
|
3
|
-
|
|
2
|
+
// Postinstall hook — auto-migrate simple OA cron jobs to systemd --user timers
|
|
3
|
+
// Safe, best-effort, idempotent. Skips if systemd --user is unavailable.
|
|
4
|
+
|
|
5
|
+
const { execSync } = require('node:child_process');
|
|
6
|
+
const { mkdirSync, writeFileSync } = require('node:fs');
|
|
7
|
+
const { join } = require('node:path');
|
|
8
|
+
|
|
9
|
+
function hasSystemdUser() {
|
|
10
|
+
try { execSync('systemctl --user --version', { stdio: 'pipe' }); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readCrontab() {
|
|
14
|
+
try { return execSync('crontab -l 2>/dev/null', { encoding: 'utf8', stdio: 'pipe' }).split('\n'); } catch { return []; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeCrontab(lines) {
|
|
18
|
+
try {
|
|
19
|
+
const { spawnSync } = require('node:child_process');
|
|
20
|
+
const r = spawnSync('crontab', ['-'], { input: lines.join('\n') + '\n', encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
|
|
21
|
+
return r.status === 0;
|
|
22
|
+
} catch { return false; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseCron(line) {
|
|
26
|
+
const m = line.trim().match(/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/);
|
|
27
|
+
if (!m) return null;
|
|
28
|
+
const [ , min, hour, dom, mon, dow, rest ] = m;
|
|
29
|
+
return { min, hour, dom, mon, dow, rest };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isOaCron(line) { return /#\s*OPEN-AGENTS-SCHEDULED:/.test(line); }
|
|
4
33
|
|
|
34
|
+
function extractId(line) {
|
|
35
|
+
const m = line.match(/OPEN-AGENTS-SCHEDULED:(\S+)/); return m ? m[1] : 'unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractWorkingDir(line) {
|
|
39
|
+
const m = line.match(/\bcd\s+(["'])([^"']+)\1/); return m ? m[2] : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripMarker(cmd) { return cmd.replace(/\s+#\s*OPEN-AGENTS-SCHEDULED:\S+.*/, ''); }
|
|
43
|
+
|
|
44
|
+
function timerSpecFromCron(min, hour, dom, mon, dow) {
|
|
45
|
+
// Support only */N minute and * * * * * → map to OnUnitActiveSec. Others return null (skip)
|
|
46
|
+
if (min === '*' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return { everySec: 60 };
|
|
47
|
+
const step = (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') ? parseInt(min.slice(2), 10) : NaN;
|
|
48
|
+
if (!Number.isNaN(step) && step > 0) return { everySec: step * 60 };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function migrate() {
|
|
53
|
+
if (process.env.OA_SKIP_AUTOMIGRATE === '1') return;
|
|
54
|
+
if (!hasSystemdUser()) return;
|
|
55
|
+
const lines = readCrontab();
|
|
56
|
+
if (!lines.length) return;
|
|
57
|
+
const unitDir = join(process.env.HOME || process.env.USERPROFILE || '.', '.config', 'systemd', 'user');
|
|
58
|
+
const kept = [];
|
|
59
|
+
let changed = 0; let created = 0;
|
|
60
|
+
for (const l of lines) {
|
|
61
|
+
if (!isOaCron(l)) { kept.push(l); continue; }
|
|
62
|
+
const id = extractId(l);
|
|
63
|
+
const cron = parseCron(l);
|
|
64
|
+
if (!cron) { kept.push(l); continue; }
|
|
65
|
+
const spec = timerSpecFromCron(cron.min, cron.hour, cron.dom, cron.mon, cron.dow);
|
|
66
|
+
if (!spec) { kept.push(l); continue; }
|
|
67
|
+
const wd = extractWorkingDir(l) || process.cwd();
|
|
68
|
+
const shell = stripMarker(cron.rest);
|
|
69
|
+
const unitBase = `oa-${id}`;
|
|
70
|
+
const svc = join(unitDir, `${unitBase}.service`);
|
|
71
|
+
const tim = join(unitDir, `${unitBase}.timer`);
|
|
72
|
+
const execCmd = `/bin/sh -lc ${JSON.stringify(shell)}`;
|
|
73
|
+
const svcText = `[Unit]\nDescription=Open Agents Scheduled Task ${id}\nAfter=default.target\n\n[Service]\nType=oneshot\nExecStart=${execCmd}\nWorkingDirectory=${wd}\nEnvironment=OA_DAEMON=1\n\n[Install]\nWantedBy=default.target\n`;
|
|
74
|
+
const timText = `[Unit]\nDescription=Timer for ${unitBase}\n\n[Timer]\nOnBootSec=30\nOnUnitActiveSec=${spec.everySec}\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n`;
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(unitDir, { recursive: true });
|
|
77
|
+
writeFileSync(svc, svcText);
|
|
78
|
+
writeFileSync(tim, timText);
|
|
79
|
+
try {
|
|
80
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
81
|
+
execSync(`systemctl --user enable --now ${unitBase}.timer`, { stdio: 'pipe' });
|
|
82
|
+
} catch {}
|
|
83
|
+
created++;
|
|
84
|
+
// Drop this cron entry since it’s now covered by timer
|
|
85
|
+
changed++;
|
|
86
|
+
} catch { kept.push(l); }
|
|
87
|
+
}
|
|
88
|
+
// If any changes, write new crontab
|
|
89
|
+
if (changed) writeCrontab(kept);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try { migrate(); } catch {}
|
|
93
|
+
process.exit(0);
|
package/package.json
CHANGED