@lifeaitools/clauth 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1588,7 +1588,13 @@ let supervisorLogTailStarted = false;
1588
1588
  function startSupervisorLogTail() {
1589
1589
  if (supervisorLogTailStarted) return; // showMain() can run more than once per page load
1590
1590
  supervisorLogTailStarted = true;
1591
- setInterval(loadSupervisorCockpit, 3000);
1591
+ // 3s was far too aggressive for a panel nobody watches continuously, and it
1592
+ // was self-defeating: each tick issues GET /health + /v1/surfaces + the log
1593
+ // read, and the request logger writes a line for each — so the dashboard was
1594
+ // the dominant author of the very log it renders. clauth-serve.log had grown
1595
+ // to 63 MB / 892,272 lines, its tail almost entirely this poll.
1596
+ // 15s keeps the panel live without the panel being the workload.
1597
+ setInterval(loadSupervisorCockpit, 15000);
1592
1598
  }
1593
1599
 
1594
1600
  function supervisorBadge(text, kind) {
@@ -1626,14 +1632,22 @@ const CLAUTH_SELF_PSEUDO_SURFACE = {
1626
1632
  health: null,
1627
1633
  };
1628
1634
 
1635
+ let supervisorCockpitLoadedOnce = false;
1629
1636
  async function loadSupervisorCockpit() {
1630
1637
  const status = document.getElementById("supervisor-status");
1631
- if (status) status.textContent = "Loading supervisor state…";
1638
+ // Only announce loading on the FIRST paint. On a background poll this
1639
+ // overwrote the healthy/vault/version badges with "Loading supervisor state…"
1640
+ // every tick, so the panel visibly flashed on a fixed cadence forever.
1641
+ if (status && !supervisorCockpitLoadedOnce) status.textContent = "Loading supervisor state…";
1632
1642
  try {
1633
- const [health, surfaces, logs] = await Promise.all([
1643
+ const [health, surfaces, logs, daemonLog] = await Promise.all([
1634
1644
  supervisorJson("/health"),
1635
1645
  supervisorJson("/v1/surfaces"),
1636
1646
  supervisorJson("/v1/logs?limit=40"),
1647
+ // The Operations card tails the clauth DAEMON log — the actual
1648
+ // clauth-serve.log an operator means by "the clauth log" — not the
1649
+ // supervisor's events.jsonl, which is a separate plugin-event stream.
1650
+ fetch(BASE + "/v1/daemon-log?lines=60").then(r => r.json()).catch(() => null),
1637
1651
  ]);
1638
1652
  const surfaceRows = [CLAUTH_SELF_PSEUDO_SURFACE, ...(surfaces.surfaces || [])];
1639
1653
  lastSupervisorSurfaceRows = surfaceRows;
@@ -1644,17 +1658,32 @@ async function loadSupervisorCockpit() {
1644
1658
  document.getElementById("supervisor-operation-count").textContent = String((logs.operations || []).length);
1645
1659
  document.getElementById("supervisor-surface-meta").textContent = "pm2 home: " + (health.pm2_home || "—");
1646
1660
  document.getElementById("supervisor-log-path").textContent = logs.log_path || "events.jsonl";
1647
- document.getElementById("supervisor-surfaces").innerHTML = surfaceRows.map(renderSupervisorSurface).join("");
1661
+ // Replacing innerHTML resets scrollTop to 0. The surfaces list is taller
1662
+ // than its box, so on every poll tick the row you were looking at — or had
1663
+ // just selected — scrolled out from under you. That is the "jumps up and
1664
+ // down": not the data changing, the scroll position being thrown away on a
1665
+ // fixed cadence. Preserve it across the swap.
1666
+ const surfacesEl = document.getElementById("supervisor-surfaces");
1667
+ const keepScroll = surfacesEl.scrollTop;
1668
+ surfacesEl.innerHTML = surfaceRows.map(renderSupervisorSurface).join("");
1669
+ surfacesEl.scrollTop = keepScroll;
1648
1670
  renderSupervisorCmdbar();
1671
+ supervisorCockpitLoadedOnce = true;
1649
1672
  const logEl = document.getElementById("supervisor-events");
1650
1673
  // Newest-at-bottom, like a real tail -- and only auto-scroll if the
1651
1674
  // reader was already at the bottom, so scrolling up to read history
1652
1675
  // during a poll tick doesn't get yanked back down.
1653
1676
  const wasAtBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 12;
1654
- logEl.textContent = (logs.events || []).slice(-40).map(e => {
1655
- const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
1656
- return (e.ts || e.created_at || "") + " " + label;
1657
- }).join("\\n") || "No events yet.";
1677
+ // Real daemon log when it is reachable; supervisor events only as a
1678
+ // fallback so the card is never blank if the tail route fails.
1679
+ if (daemonLog && Array.isArray(daemonLog.lines) && daemonLog.lines.length) {
1680
+ logEl.textContent = daemonLog.lines.join("\\n");
1681
+ } else {
1682
+ logEl.textContent = (logs.events || []).slice(-40).map(e => {
1683
+ const label = e.kind === "operation" ? (e.action + " " + JSON.stringify(e.target || {})) : (e.kind + " " + (e.plugin_id || ""));
1684
+ return (e.ts || e.created_at || "") + " " + label;
1685
+ }).join("\\n") || "No events yet.";
1686
+ }
1658
1687
  if (wasAtBottom) logEl.scrollTop = logEl.scrollHeight;
1659
1688
  if (status) status.innerHTML = supervisorBadge("supervisor " + (health.status === "ok" ? "healthy" : (health.status || "unknown")), health.status === "ok" ? "ok" : "warn") + supervisorBadge("vault " + (health.vault_locked ? "locked" : "unlocked"), health.vault_locked ? "warn" : "ok") + supervisorBadge("v" + (health.clauth_version || "${VERSION}"), "");
1660
1689
  } catch (err) {
@@ -4498,6 +4527,48 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4498
4527
  return ok(res, { tunnels: listTunnels() });
4499
4528
  }
4500
4529
 
4530
+ // GET /v1/daemon-log — tail of THE clauth daemon log (clauth-serve.log),
4531
+ // which is what an operator means by "the clauth log". /v1/logs below is a
4532
+ // different thing: supervisor plugin events out of events.jsonl. The
4533
+ // dashboard's Operations card rendered that second file, so it showed
4534
+ // plugin_discovered/credential_required chatter and never the daemon's own
4535
+ // activity.
4536
+ //
4537
+ // Reads only the LAST slice of the file, never the whole thing: this log is
4538
+ // routinely tens of megabytes (63 MB / 892,272 lines when this was written),
4539
+ // so a naive readFileSync would stall the daemon on every poll.
4540
+ if (method === "GET" && reqPath === "/v1/daemon-log") {
4541
+ const want = Number(url.searchParams.get("lines") || 60);
4542
+ const lines = Number.isFinite(want) ? Math.max(1, Math.min(want, 500)) : 60;
4543
+ try {
4544
+ const stat = fs.statSync(LOG_FILE);
4545
+ const TAIL_BYTES = 96 * 1024;
4546
+ const start = Math.max(0, stat.size - TAIL_BYTES);
4547
+ const fd = fs.openSync(LOG_FILE, "r");
4548
+ const buf = Buffer.alloc(Math.min(TAIL_BYTES, stat.size));
4549
+ fs.readSync(fd, buf, 0, buf.length, start);
4550
+ fs.closeSync(fd);
4551
+ const text = buf.toString("utf8");
4552
+ // Drop the first line when we started mid-file — it is a partial line.
4553
+ const all = text.split(/\r?\n/).filter(Boolean);
4554
+ const rows = (start > 0 ? all.slice(1) : all).slice(-lines);
4555
+ return ok(res, {
4556
+ schema: "clauth.daemon.log.v1",
4557
+ log_path: LOG_FILE,
4558
+ size_bytes: stat.size,
4559
+ lines: rows,
4560
+ });
4561
+ } catch (err) {
4562
+ return ok(res, {
4563
+ schema: "clauth.daemon.log.v1",
4564
+ log_path: LOG_FILE,
4565
+ size_bytes: 0,
4566
+ lines: [],
4567
+ error: err instanceof Error ? err.message : String(err),
4568
+ });
4569
+ }
4570
+ }
4571
+
4501
4572
  if (method === "GET" && reqPath === "/v1/logs") {
4502
4573
  const limit = Number(url.searchParams.get("limit") || 100);
4503
4574
  const boundedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 100;
@@ -500,6 +500,23 @@ export function registerPlugin(manifestPath, actor = "localhost") {
500
500
  ok: false, state: "manifest_unreadable", error: error instanceof Error ? error.message : String(error),
501
501
  }, actor);
502
502
  }
503
+ // ${PACKAGE_ROOT} — "wherever this manifest actually landed" — resolved HERE
504
+ // and baked into the stored copy, which is the only point at which the true
505
+ // origin is still known. registerPlugin copies the manifest into the managed
506
+ // root, so by the time discoverPlugins() re-reads it, sourcePath is the
507
+ // managed directory and the package's real install location is gone. An npm
508
+ // package installs at node_modules/@scope/name/, a path nothing can hardcode.
509
+ //
510
+ // Without this a shipped manifest had to name an absolute path: rdc-skills
511
+ // carried cwd "C:/Dev/rdc-skills" with core+enable_default, so `npm i -g` on
512
+ // any other machine auto-enabled a CORE plugin pointing at a directory that
513
+ // does not exist there. The token, not the package, was the defect — the
514
+ // correct value was simply inexpressible.
515
+ const packageRoot = path.dirname(path.resolve(manifestPath)).replaceAll("\\", "/");
516
+ raw = raw
517
+ .replaceAll("${PACKAGE_ROOT}", packageRoot)
518
+ .replaceAll("$PACKAGE_ROOT", packageRoot)
519
+ .replace(/%PACKAGE_ROOT%/gi, packageRoot);
503
520
  let manifest;
504
521
  try {
505
522
  manifest = validatePluginManifest(JSON.parse(raw), manifestPath);
@@ -836,6 +853,29 @@ export function operation(action, target, prior, result, actor = "localhost") {
836
853
  state.operations = [receipt, ...(state.operations || [])].slice(0, 500);
837
854
  saveSupervisorState(state);
838
855
  appendJsonl(file("events.jsonl"), { ts: now(), kind: "operation", ...receipt });
856
+ // Mirror one human-readable line into the clauth DAEMON log as well.
857
+ //
858
+ // events.jsonl stays the structured record that /v1/logs and state read.
859
+ // But an operator asking for "the clauth log" means clauth-serve.log, and
860
+ // until now supervisor operations never appeared there — so a surface
861
+ // start/stop/restart was invisible in the log people actually tail, and the
862
+ // dashboard had to render a second, separate stream to show them at all.
863
+ // One log, with the events in it.
864
+ //
865
+ // Best-effort by construction: a logging failure must never fail the
866
+ // operation whose receipt this is.
867
+ try {
868
+ const logFile = process.env.CLAUTH_SERVE_LOG
869
+ || path.join(os.tmpdir(), "clauth-serve.log");
870
+ if (fs.existsSync(path.dirname(logFile))) {
871
+ const okFlag = result?.ok === false ? "FAIL" : "ok";
872
+ const tgt = target && typeof target === "object"
873
+ ? (target.surface_id || target.plugin_id || target.service || JSON.stringify(target))
874
+ : String(target ?? "");
875
+ const stateLabel = result?.state ? ` ${result.state}` : "";
876
+ fs.appendFileSync(logFile, `[${receipt.created_at}] ${action} ${tgt} ${okFlag}${stateLabel} actor=${actor}\n`, "utf8");
877
+ }
878
+ } catch { /* logging must never break the operation it records */ }
839
879
  return receipt;
840
880
  }
841
881