@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.
- package/cli/commands/serve.js +79 -8
- package/cli/supervisor-registry.js +40 -0
- package/cli/supervisor-registry.test.js +964 -926
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
|