open-agents-ai 0.187.314 → 0.187.317

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
@@ -299476,6 +299500,35 @@ The session corrections MUST become hard rules in the SKILL.md Rules section.`;
299476
299500
  }
299477
299501
  return "handled";
299478
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
+ }
299479
299532
  if (sub === "list") {
299480
299533
  try {
299481
299534
  const r2 = await doFetch("/v1/scheduled");
@@ -320640,6 +320693,7 @@ body {
320640
320693
  <div id="dashboard-health" style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap"></div>
320641
320694
  <div id="dashboard-daemons" style="margin-bottom:16px"></div>
320642
320695
  <div id="dashboard-scheduled" style="margin-bottom:16px"></div>
320696
+ <div id="dashboard-services" style="margin-bottom:16px"></div>
320643
320697
  <div id="dashboard-usage" style="margin-bottom:16px"></div>
320644
320698
  <h3 style="color:#b2920a;font-size:0.7rem;margin-bottom:8px">Job History</h3>
320645
320699
  <div id="jobs-list" style="font-size:0.78rem"></div>
@@ -321822,7 +321876,7 @@ async function loadDaemons() {
321822
321876
  // Scheduled jobs panel
321823
321877
  async function loadScheduled() {
321824
321878
  try {
321825
- const r = await fetch('/v1/scheduled', { headers: headers() });
321879
+ const r = await fetch('/v1/scheduled/status', { headers: headers() });
321826
321880
  const d = await r.json();
321827
321881
  const el = document.getElementById('dashboard-scheduled');
321828
321882
  if (!el) return;
@@ -321837,10 +321891,13 @@ async function loadScheduled() {
321837
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>'
321838
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>';
321839
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>';
321840
321897
  const row = '<div style="background:#1e1e22;border-left:2px solid ' + color + ';padding:6px 10px;margin:4px 0;font-size:0.72rem">'
321841
- + '<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>'
321842
321899
  + '<div style="color:#555;font-size:0.6rem">' + t.file + '#' + t.index + '</div>'
321843
- + '<div style="margin-top:4px">' + btn + '</div>'
321900
+ + '<div style="margin-top:4px;display:flex;gap:8px">' + btn + killBtn + '</div>'
321844
321901
  + '</div>';
321845
321902
  return row;
321846
321903
  }).join('');
@@ -321849,6 +321906,9 @@ async function loadScheduled() {
321849
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>'
321850
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>'
321851
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>'
321852
321912
  + '</div>';
321853
321913
  } catch {}
321854
321914
  }
@@ -321916,6 +321976,110 @@ No remaining matched processes.';
321916
321976
  } catch {}
321917
321977
  }
321918
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
+
321919
322083
  // Agent task
321920
322084
  let currentRunId = null;
321921
322085
  async function loadProfiles() {
@@ -323046,6 +323210,7 @@ async function doUpdate() {
323046
323210
  try { checkHealth(); } catch {}
323047
323211
  try { pollMetrics(); } catch {}
323048
323212
  try { loadScheduled(); } catch {}
323213
+ try { loadServices(); } catch {}
323049
323214
 
323050
323215
  btn.textContent = 'updated v' + newVersion;
323051
323216
  btn.style.background = '#1a3a1a';
@@ -323680,6 +323845,7 @@ restoreChatSession(); // WO-CHAT-RESUME — rehydrate from server state
323680
323845
  setInterval(checkHealth, 30000);
323681
323846
  setInterval(pollMetrics, 10000);
323682
323847
  setInterval(loadScheduled, 15000);
323848
+ setInterval(loadServices, 30000);
323683
323849
  setInterval(pollVersionBump, 5000);
323684
323850
  input.focus();
323685
323851
  </script>
@@ -327682,6 +327848,37 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
327682
327848
  });
327683
327849
  return;
327684
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
+ }
327685
327882
  if ((pathname === "/v1/chat" || pathname === "/api/chat") && method === "POST") {
327686
327883
  if (!checkAuth(req2, res, "run")) {
327687
327884
  status = 401;
@@ -328633,6 +328830,16 @@ function setScheduledEnabled(id, enabled2) {
328633
328830
  } else {
328634
328831
  writeFileSync45(target.file, JSON.stringify({ tasks: arr }, null, 2));
328635
328832
  }
328833
+ if (!enabled2) {
328834
+ try {
328835
+ removeCronByMarker(id);
328836
+ } catch {
328837
+ }
328838
+ try {
328839
+ disableUserTimerById(id);
328840
+ } catch {
328841
+ }
328842
+ }
328636
328843
  return true;
328637
328844
  } catch {
328638
328845
  return false;
@@ -328692,16 +328899,17 @@ function listMatchingProcesses(pattern) {
328692
328899
  try {
328693
328900
  const { execSync: es } = __require("node:child_process");
328694
328901
  const re = new RegExp(pattern, "i");
328695
- const ps = es("ps -eo pid,pcpu,pmem,command", { encoding: "utf8", stdio: "pipe" });
328902
+ const ps = es("ps -eo pid,etimes,pcpu,pmem,command", { encoding: "utf8", stdio: "pipe" });
328696
328903
  for (const line of ps.split("\n")) {
328697
- const m2 = line.trim().match(/^(\d+)\s+([0-9.]+)?\s+([0-9.]+)?\s+(.+)$/);
328904
+ const m2 = line.trim().match(/^(\d+)\s+(\d+)?\s+([0-9.]+)?\s+([0-9.]+)?\s+(.+)$/);
328698
328905
  if (!m2) continue;
328699
328906
  const pid = parseInt(m2[1], 10);
328700
- const cpu = m2[2] ? parseFloat(m2[2]) : null;
328701
- const mem = m2[3] ? parseFloat(m2[3]) : null;
328702
- const cmd = m2[4] || "";
328907
+ const et = m2[2] ? parseInt(m2[2], 10) : void 0;
328908
+ const cpu = m2[3] ? parseFloat(m2[3]) : null;
328909
+ const mem = m2[4] ? parseFloat(m2[4]) : null;
328910
+ const cmd = m2[5] || "";
328703
328911
  if (!isFinite(pid)) continue;
328704
- if (re.test(cmd)) list.push({ pid, cpu, mem, cmd });
328912
+ if (re.test(cmd)) list.push({ pid, cpu, mem, cmd, uptime_s: et });
328705
328913
  }
328706
328914
  } catch {
328707
328915
  }
@@ -328724,6 +328932,278 @@ function sampleGpuUtil() {
328724
328932
  return null;
328725
328933
  }
328726
328934
  }
328935
+ function getCurrentCrontabLines() {
328936
+ try {
328937
+ const { execSync: es } = __require("node:child_process");
328938
+ return es("crontab -l 2>/dev/null", { encoding: "utf8", stdio: "pipe" }).split("\n");
328939
+ } catch {
328940
+ return [];
328941
+ }
328942
+ }
328943
+ function detectLegacyCron() {
328944
+ const lines = getCurrentCrontabLines();
328945
+ const out = [];
328946
+ for (const l2 of lines) {
328947
+ if (!l2.includes(CRON_MARKER2)) continue;
328948
+ const idMatch = l2.match(new RegExp(`${CRON_MARKER2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\S+)`));
328949
+ const id = idMatch?.[1] || "unknown";
328950
+ const parts = l2.trim().split(/\s+/);
328951
+ if (parts.length < 6) continue;
328952
+ const cron = parts.slice(0, 5).join(" ");
328953
+ let workingDir = null;
328954
+ const cdMatch = l2.match(/\bcd\s+(["'])([^"']+)\1/);
328955
+ if (cdMatch) workingDir = cdMatch[2];
328956
+ let task = null;
328957
+ try {
328958
+ const afterCd = l2.split(/\bcd\s+["'][^"']+["']/)[1] || l2;
328959
+ const oaIdx = afterCd.search(/\boa\b|open-agents-ai/);
328960
+ if (oaIdx >= 0) {
328961
+ const tail = afterCd.slice(oaIdx);
328962
+ const segMatch = tail.match(/['"][\s\S]*?>>/);
328963
+ if (segMatch) {
328964
+ let seg = segMatch[0];
328965
+ seg = seg.replace(/>>[\s\S]*/, "");
328966
+ seg = seg.replace(/'\\''/g, "\0");
328967
+ seg = seg.replace(/['"]/g, "");
328968
+ seg = seg.replace(/\u0000/g, "'");
328969
+ task = seg;
328970
+ }
328971
+ }
328972
+ } catch {
328973
+ }
328974
+ out.push({ id, cron, workingDir, task, line: l2 });
328975
+ }
328976
+ return out;
328977
+ }
328978
+ function reconcileScheduledTasks(apply) {
328979
+ const found = detectLegacyCron();
328980
+ const adopted = [];
328981
+ const mismatches = [];
328982
+ const errors = [];
328983
+ for (const f2 of found) {
328984
+ const wdir = f2.workingDir || process.cwd();
328985
+ const file = join99(wdir, ".oa", "scheduled", "tasks.json");
328986
+ try {
328987
+ let json = { tasks: [] };
328988
+ try {
328989
+ const raw = readFileSync65(file, "utf-8");
328990
+ json = JSON.parse(raw);
328991
+ } catch {
328992
+ }
328993
+ const arr = Array.isArray(json?.tasks) ? json.tasks : Array.isArray(json) ? json : [];
328994
+ const exists2 = arr.some((t2) => String(t2?.schedule || t2?.cron || "") === f2.cron && String(t2?.task || t2?.name || t2?.command || "") === (f2.task || ""));
328995
+ if (!exists2) {
328996
+ if (apply) {
328997
+ const entry = { task: f2.task || `legacy ${f2.id}`, schedule: f2.cron, enabled: true };
328998
+ arr.push(entry);
328999
+ const toWrite = Array.isArray(json?.tasks) ? { ...json, tasks: arr } : Array.isArray(json) ? arr : { tasks: arr };
329000
+ mkdirSync51(join99(wdir, ".oa", "scheduled"), { recursive: true });
329001
+ mkdirSync51(join99(wdir, ".oa", "scheduled", "logs"), { recursive: true });
329002
+ writeFileSync45(file, JSON.stringify(toWrite, null, 2));
329003
+ adopted.push({ file, index: arr.length - 1 });
329004
+ }
329005
+ } else {
329006
+ const idx = arr.findIndex((t2) => String(t2?.schedule || t2?.cron || "") === f2.cron && String(t2?.task || t2?.name || t2?.command || "") === (f2.task || ""));
329007
+ if (idx >= 0) {
329008
+ const en = typeof arr[idx].enabled === "boolean" ? arr[idx].enabled : true;
329009
+ if (!en) mismatches.push({ file, id: f2.id, reason: "cron present but task disabled in tasks.json" });
329010
+ }
329011
+ }
329012
+ } catch (e2) {
329013
+ errors.push({ line: f2.line, error: e2?.message || String(e2) });
329014
+ }
329015
+ }
329016
+ return { found: found.map(({ id, cron, workingDir, task }) => ({ id, cron, workingDir, task })), adopted, mismatches, errors };
329017
+ }
329018
+ function findOaBinary4() {
329019
+ try {
329020
+ const { execSync: es } = __require("node:child_process");
329021
+ for (const cmd of ["oa", "open-agents"]) {
329022
+ try {
329023
+ const p2 = es(`which ${cmd} 2>/dev/null`, { encoding: "utf8", stdio: "pipe" }).trim();
329024
+ if (p2) return p2;
329025
+ } catch {
329026
+ }
329027
+ }
329028
+ } catch {
329029
+ }
329030
+ return "npx open-agents-ai";
329031
+ }
329032
+ function getCrontabLines() {
329033
+ return getCurrentCrontabLines();
329034
+ }
329035
+ function writeCrontabLines(lines) {
329036
+ try {
329037
+ const { spawnSync: spawnSync6 } = __require("node:child_process");
329038
+ const result = spawnSync6("crontab", ["-"], { input: lines.join("\n") + "\n", encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
329039
+ if (result.status !== 0) throw new Error(`crontab exited ${result.status}: ${String(result.stderr || "").trim()}`);
329040
+ } catch (e2) {
329041
+ throw new Error(`writeCrontab failed: ${e2 instanceof Error ? e2.message : String(e2)}`);
329042
+ }
329043
+ }
329044
+ function canonicalCronLine(rec) {
329045
+ const oaBin = findOaBinary4();
329046
+ const logDir = join99(rec.workingDir, ".oa", "scheduled", "logs");
329047
+ const logFile = join99(logDir, `${rec.id}.log`);
329048
+ const storeFile = join99(rec.workingDir, ".oa", "scheduled", "tasks.json");
329049
+ const taskEsc = rec.task.replace(/'/g, "'\\''");
329050
+ const lockDir = join99(rec.workingDir, ".oa", "run");
329051
+ const lockPath = join99(lockDir, `${rec.id}.lock`);
329052
+ const wrapper = [
329053
+ `cd ${JSON.stringify(rec.workingDir)}`,
329054
+ `mkdir -p ${JSON.stringify(logDir)}`,
329055
+ `mkdir -p ${JSON.stringify(lockDir)}`,
329056
+ `if mkdir ${JSON.stringify(lockPath)} 2>/dev/null; then`,
329057
+ ` echo $$ > ${JSON.stringify(join99(lockPath, "pid"))}`,
329058
+ ` trap 'rm -rf ${lockPath}' EXIT`,
329059
+ `else`,
329060
+ ` if [ -f ${JSON.stringify(join99(lockPath, "pid"))} ]; then`,
329061
+ ` oldpid=$(cat ${JSON.stringify(join99(lockPath, "pid"))} 2>/dev/null || echo)`,
329062
+ ` if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then`,
329063
+ ` echo "[oa-scheduler] ${rec.id} already running as PID $oldpid; skipping" >> ${JSON.stringify(logFile)}`,
329064
+ ` exit 0`,
329065
+ ` else`,
329066
+ ` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
329067
+ ` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join99(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
329068
+ ` fi`,
329069
+ ` else`,
329070
+ ` rm -rf ${JSON.stringify(lockPath)} 2>/dev/null || true`,
329071
+ ` mkdir -p ${JSON.stringify(lockPath)} && echo $$ > ${JSON.stringify(join99(lockPath, "pid"))} && trap 'rm -rf ${lockPath}' EXIT`,
329072
+ ` fi`,
329073
+ `fi`,
329074
+ `${oaBin} '${taskEsc}' >> ${JSON.stringify(logFile)} 2>&1; _oa_exit=$?`,
329075
+ `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`
329076
+ ].join("; ");
329077
+ return `${rec.cron} ${wrapper} ${CRON_MARKER2}${rec.id}`;
329078
+ }
329079
+ function fixupOrMigrateScheduled(mode, dryRun) {
329080
+ const found = detectLegacyCron();
329081
+ const changes = [];
329082
+ const errors = [];
329083
+ if (mode === "cron") {
329084
+ try {
329085
+ const lines = getCrontabLines();
329086
+ let next = lines.filter((l2) => !l2.includes(CRON_MARKER2));
329087
+ for (const f2 of found) {
329088
+ if (!f2.workingDir || !f2.task) continue;
329089
+ const rec = { id: f2.id, cron: f2.cron, workingDir: f2.workingDir, task: f2.task };
329090
+ const newline = canonicalCronLine(rec);
329091
+ next.push(newline);
329092
+ changes.push({ id: f2.id, action: dryRun ? "would-rewrite" : "rewrite", detail: "cron line canonicalized" });
329093
+ }
329094
+ if (!dryRun) writeCrontabLines(next);
329095
+ } catch (e2) {
329096
+ errors.push({ error: e2?.message || String(e2) });
329097
+ }
329098
+ } else if (mode === "migrate" || mode === "systemd") {
329099
+ for (const f2 of found) {
329100
+ try {
329101
+ if (!f2.workingDir || !f2.task) continue;
329102
+ const unitBase = `oa-${f2.id}`;
329103
+ const unitDir = join99(homedir38(), ".config", "systemd", "user");
329104
+ const svc = join99(unitDir, `${unitBase}.service`);
329105
+ const tim = join99(unitDir, `${unitBase}.timer`);
329106
+ const oaBin = findOaBinary4();
329107
+ const rec = { id: f2.id, cron: f2.cron, workingDir: f2.workingDir, task: f2.task };
329108
+ const cmd = canonicalCronLine(rec).split(" ").slice(5).join(" ");
329109
+ const execCmd = `/bin/sh -lc ${JSON.stringify(cmd.replace(/\s+${CRON_MARKER}.+$/, "").trim())}`;
329110
+ const svcText = `[Unit]
329111
+ Description=Open Agents Scheduled Task ${f2.id}
329112
+ After=default.target
329113
+
329114
+ [Service]
329115
+ Type=oneshot
329116
+ ExecStart=${execCmd}
329117
+ WorkingDirectory=${f2.workingDir}
329118
+ Environment=OA_DAEMON=1
329119
+
329120
+ [Install]
329121
+ WantedBy=default.target
329122
+ `;
329123
+ const onCal = f2.cron.trim() === "* * * * *" ? "*:*:00" : f2.cron.trim().startsWith("*/") ? `*:0/${f2.cron.trim().split(/\s+/)[0].slice(2)}:00` : "*-*-* *:*:00";
329124
+ const timText = `[Unit]
329125
+ Description=Timer for ${unitBase}
329126
+
329127
+ [Timer]
329128
+ OnCalendar=${onCal}
329129
+ Persistent=true
329130
+
329131
+ [Install]
329132
+ WantedBy=timers.target
329133
+ `;
329134
+ if (!dryRun) {
329135
+ mkdirSync51(unitDir, { recursive: true });
329136
+ writeFileSync45(svc, svcText);
329137
+ writeFileSync45(tim, timText);
329138
+ try {
329139
+ const { execSync: es } = __require("node:child_process");
329140
+ es("systemctl --user daemon-reload", { stdio: "pipe" });
329141
+ es(`systemctl --user enable --now ${unitBase}.timer`, { stdio: "pipe" });
329142
+ } catch {
329143
+ }
329144
+ }
329145
+ changes.push({ id: f2.id, action: dryRun ? "would-create-timer" : "create-timer", detail: `${unitBase}.timer` });
329146
+ } catch (e2) {
329147
+ errors.push({ id: f2.id, error: e2?.message || String(e2) });
329148
+ }
329149
+ }
329150
+ if (mode === "migrate") {
329151
+ try {
329152
+ const lines = getCrontabLines();
329153
+ const next = lines.filter((l2) => !l2.includes(CRON_MARKER2));
329154
+ if (!dryRun) writeCrontabLines(next);
329155
+ changes.push({ id: "*", action: dryRun ? "would-remove-cron" : "remove-cron", detail: "removed all OA cron entries" });
329156
+ } catch (e2) {
329157
+ errors.push({ error: e2?.message || String(e2) });
329158
+ }
329159
+ }
329160
+ }
329161
+ return { changes, errors };
329162
+ }
329163
+ function listUserServices(pattern) {
329164
+ const out = [];
329165
+ try {
329166
+ const { execSync: es } = __require("node:child_process");
329167
+ const files = es("systemctl --user list-unit-files --type=service --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
329168
+ const enabledMap = /* @__PURE__ */ new Map();
329169
+ for (const line of files) {
329170
+ const m2 = line.trim().match(/^(\S+)\s+(\S+)/);
329171
+ if (!m2) continue;
329172
+ const name11 = m2[1], state = m2[2];
329173
+ if (pattern.test(name11)) enabledMap.set(name11, state);
329174
+ }
329175
+ const units = es("systemctl --user list-units --type=service --all --no-legend", { encoding: "utf8", stdio: "pipe" }).trim().split("\n");
329176
+ const activeMap = /* @__PURE__ */ new Map();
329177
+ for (const line of units) {
329178
+ const m2 = line.trim().match(/^(\S+)\s+\S+\s+\S+\s+(\S+)/);
329179
+ if (!m2) continue;
329180
+ const name11 = m2[1], state = m2[2];
329181
+ if (pattern.test(name11)) activeMap.set(name11, state);
329182
+ }
329183
+ for (const [name11, en] of enabledMap.entries()) {
329184
+ out.push({ name: name11, enabled: en, active: activeMap.get(name11) || "inactive" });
329185
+ }
329186
+ } catch {
329187
+ }
329188
+ return out;
329189
+ }
329190
+ function userServiceAction(unit, action) {
329191
+ try {
329192
+ const { execSync: es } = __require("node:child_process");
329193
+ if (action === "stop") {
329194
+ es(`systemctl --user stop ${unit}`, { stdio: "pipe" });
329195
+ return true;
329196
+ } else if (action === "disable") {
329197
+ es(`systemctl --user disable ${unit}`, { stdio: "pipe" });
329198
+ return true;
329199
+ } else if (action === "restart") {
329200
+ es(`systemctl --user restart ${unit}`, { stdio: "pipe" });
329201
+ return true;
329202
+ }
329203
+ } catch {
329204
+ }
329205
+ return false;
329206
+ }
328727
329207
  function startApiServer(options2 = {}) {
328728
329208
  if (options2.quiet) setQuiet(true);
328729
329209
  const log22 = options2.quiet ? (_msg) => {
@@ -329271,7 +329751,24 @@ async function apiServeCommand(opts, config) {
329271
329751
  server.on("close", resolve40);
329272
329752
  });
329273
329753
  }
329274
- var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses, perKeyUsage;
329754
+ function removeCronByMarker(id) {
329755
+ try {
329756
+ const lines = getCurrentCrontabLines();
329757
+ if (!lines.length) return;
329758
+ const next = lines.filter((l2) => !l2.includes(`${CRON_MARKER2}${id}`));
329759
+ if (next.length !== lines.length) writeCrontabLines(next);
329760
+ } catch {
329761
+ }
329762
+ }
329763
+ function disableUserTimerById(id) {
329764
+ try {
329765
+ const unit = `oa-${id}.timer`;
329766
+ const { execSync: es } = __require("node:child_process");
329767
+ es(`systemctl --user disable --now ${unit}`, { stdio: "pipe" });
329768
+ } catch {
329769
+ }
329770
+ }
329771
+ var endpointRegistry, modelRouteMap, endpointUsage, metrics, startedAt, _corsOrigins, _corsLocalOnly, runningProcesses, perKeyUsage, CRON_MARKER2;
329275
329772
  var init_serve = __esm({
329276
329773
  "packages/cli/src/api/serve.ts"() {
329277
329774
  "use strict";
@@ -329308,6 +329805,7 @@ var init_serve = __esm({
329308
329805
  _corsLocalOnly = _corsOrigins.length === 0;
329309
329806
  runningProcesses = /* @__PURE__ */ new Map();
329310
329807
  perKeyUsage = /* @__PURE__ */ new Map();
329808
+ CRON_MARKER2 = "# OPEN-AGENTS-SCHEDULED:";
329311
329809
  }
329312
329810
  });
329313
329811
 
@@ -1,4 +1,99 @@
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
+ // Map simple intervals to OnUnitActiveSec
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
+ // Hourly at minute 0
50
+ if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return { onCalendar: '*-*-* *:00:00' };
51
+ // Daily at fixed hour
52
+ if (min === '0' && /^\d+$/.test(hour) && dom === '*' && mon === '*' && dow === '*') return { onCalendar: `*-*-* ${hour.padStart ? hour.padStart(2,'0') : hour}:00:00` };
53
+ return null;
54
+ }
55
+
56
+ function migrate() {
57
+ if (process.env.OA_SKIP_AUTOMIGRATE === '1') return;
58
+ if (!hasSystemdUser()) return;
59
+ const lines = readCrontab();
60
+ if (!lines.length) return;
61
+ const unitDir = join(process.env.HOME || process.env.USERPROFILE || '.', '.config', 'systemd', 'user');
62
+ const kept = [];
63
+ let changed = 0; let created = 0;
64
+ for (const l of lines) {
65
+ if (!isOaCron(l)) { kept.push(l); continue; }
66
+ const id = extractId(l);
67
+ const cron = parseCron(l);
68
+ if (!cron) { kept.push(l); continue; }
69
+ const spec = timerSpecFromCron(cron.min, cron.hour, cron.dom, cron.mon, cron.dow);
70
+ if (!spec) { kept.push(l); continue; }
71
+ const wd = extractWorkingDir(l) || process.cwd();
72
+ const shell = stripMarker(cron.rest);
73
+ const unitBase = `oa-${id}`;
74
+ const svc = join(unitDir, `${unitBase}.service`);
75
+ const tim = join(unitDir, `${unitBase}.timer`);
76
+ const execCmd = `/bin/sh -lc ${JSON.stringify(shell)}`;
77
+ 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`;
78
+ const timText = spec.onCalendar
79
+ ? `[Unit]\nDescription=Timer for ${unitBase}\n\n[Timer]\nOnCalendar=${spec.onCalendar}\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n`
80
+ : `[Unit]\nDescription=Timer for ${unitBase}\n\n[Timer]\nOnBootSec=30\nOnUnitActiveSec=${spec.everySec}\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n`;
81
+ try {
82
+ mkdirSync(unitDir, { recursive: true });
83
+ writeFileSync(svc, svcText);
84
+ writeFileSync(tim, timText);
85
+ try {
86
+ execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
87
+ execSync(`systemctl --user enable --now ${unitBase}.timer`, { stdio: 'pipe' });
88
+ } catch {}
89
+ created++;
90
+ // Drop this cron entry since it’s now covered by timer
91
+ changed++;
92
+ } catch { kept.push(l); }
93
+ }
94
+ // If any changes, write new crontab
95
+ if (changed) writeCrontab(kept);
96
+ }
97
+
98
+ try { migrate(); } catch {}
99
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.314",
3
+ "version": "0.187.317",
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",