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 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
- key: t2.id,
299369
- label: `${t2.enabled ? "●" : "○"} ${t2.name || "(task)"} ${t2.schedule ? "[" + t2.schedule + "]" : ""}`,
299370
- detail: `${t2.file}#${t2.index}`
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></div>'
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 shim no-op by default
3
- process.exit(0);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.313",
3
+ "version": "0.187.316",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",