@lifeaitools/clauth 2.15.5 → 2.15.6

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.
@@ -32,6 +32,15 @@
32
32
  .supervisor-pill.ok{border-color:rgba(74,222,128,.35);color:var(--green-light);background:rgba(34,197,94,.08)}
33
33
  .supervisor-pill.warn{border-color:rgba(250,204,21,.35);color:var(--gold-light);background:rgba(250,204,21,.08)}
34
34
  .supervisor-pill.bad{border-color:rgba(248,113,113,.35);color:var(--pink);background:rgba(248,113,113,.08)}
35
+ /* NEEDS HELP -- a surface that exhausted MAX_CONSECUTIVE_RECONCILE_FAILURES
36
+ and clauth has stopped auto-restarting. Distinct from .bad ("down right
37
+ now, clauth is still retrying"): this is "clauth gave up, a human/agent
38
+ has to look." Pulses so it reads as an alarm among a fleet that regularly
39
+ has a few surfaces transiently down in .bad. */
40
+ .supervisor-pill.critical{border-color:rgba(248,113,113,.6);color:#fff;background:rgba(220,38,38,.55);animation:supervisor-pulse 1.4s ease-in-out infinite}
41
+ .supervisor-row.needs-help{border-color:rgba(220,38,38,.6);box-shadow:0 0 0 1px rgba(220,38,38,.35) inset}
42
+ .supervisor-meta.critical{color:var(--pink);white-space:normal}
43
+ @keyframes supervisor-pulse{0%,100%{opacity:1}50%{opacity:.55}}
35
44
  /* Card GRID, not a stack. flex-direction:column gave one full-width row per
36
45
  surface, so ten surfaces meant ten rows and constant scrolling to see the
37
46
  fleet. auto-fill + minmax packs as many small cards per row as the panel
@@ -305,7 +305,15 @@ class ClauthSurfacesPanel extends window.ClauthPanelElement {
305
305
  renderSupervisorSurface(surface) {
306
306
  const compositeId = surface.plugin_id + ":" + surface.id;
307
307
  const ownerKind = surface.lifecycle_owner === "clauth" ? "ok" : (surface.lifecycle_owner === "plugin" ? "warn" : "");
308
- const stateKind = surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn");
308
+ // "needs_help" is a DIFFERENT alarm than "bad": bad means "down right now,
309
+ // clauth is still retrying"; needs_help means "clauth gave up after
310
+ // MAX_CONSECUTIVE_RECONCILE_FAILURES — a human/agent has to look." It gets
311
+ // its own loud, pulsing pill (see .supervisor-pill.critical) rather than
312
+ // reusing "bad", which would render as just another red flicker in a fleet
313
+ // that regularly has a few surfaces transiently down.
314
+ const needsHelp = surface.state === "needs_help";
315
+ const stateKind = needsHelp ? "critical" : (surface.state === "current" || surface.status === "healthy" ? "ok" : (surface.state === "unavailable" ? "bad" : "warn"));
316
+ const stateLabel = needsHelp ? "NEEDS HELP" : (surface.state || surface.status || "unknown");
309
317
  const openUrl = surface.__pseudo ? null : openUiUrlForSurface(surface);
310
318
  const isSelected = this.selectedSupervisorSurface && this.selectedSupervisorSurface.id === compositeId;
311
319
  const rowClass = "supervisor-row" + (surface.__pseudo ? " readonly" : "") + (isSelected ? " selected" : "");
@@ -339,14 +347,22 @@ class ClauthSurfacesPanel extends window.ClauthPanelElement {
339
347
  : (surface.last_health_ok === false && surface.last_health_error
340
348
  ? surface.last_health_error + " · " + (surface.health || "no health")
341
349
  : (surface.health || surface.health_url || "no health"));
342
- return '<div class="' + rowClass + '" data-surface-id="' + htmlEscape(compositeId) + '"' + rowAction + '>' +
350
+ // The loud line: WHY it needs help and WHAT to do, spelled out on the
351
+ // card itself — not just a pill label a reader has to already know the
352
+ // meaning of. Mirrors the same wording reconcileSurfaceHealth() writes
353
+ // into the daemon log, so the card and the log never disagree.
354
+ const needsHelpLine = needsHelp
355
+ ? "Stopped auto-restarting after " + (surface.consecutive_reconcile_failures || "several") + " failed attempts — check the log, fix the cause, then Start/Restart to resume auto-repair."
356
+ : null;
357
+ return '<div class="' + rowClass + (needsHelp ? " needs-help" : "") + '" data-surface-id="' + htmlEscape(compositeId) + '"' + rowAction + '>' +
343
358
  '<div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.name || compositeId) + '</span>' +
344
359
  healthPill +
345
- supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) +
360
+ supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(stateLabel, stateKind) +
346
361
  openBtn +
347
362
  '</div>' +
348
363
  '<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
349
364
  '<div class="supervisor-meta">' + htmlEscape(healthLine) + '</div>' +
365
+ (needsHelpLine ? '<div class="supervisor-meta critical">' + htmlEscape(needsHelpLine) + '</div>' : '') +
350
366
  '</div>';
351
367
  }
352
368
 
@@ -18,6 +18,22 @@ const ACTIONS = new Set(["start", "stop", "restart", "reconcile", "test", "promo
18
18
  const DEFAULT_HEALTH_RECONCILE_INTERVAL_MS = 10000;
19
19
  const DEFAULT_HEALTH_TIMEOUT_MS = 2500;
20
20
  const HEALTH_RECONCILE_COOLDOWN_MS = 15000;
21
+ // After this many CONSECUTIVE failed reconcile attempts (each gated by
22
+ // HEALTH_RECONCILE_COOLDOWN_MS, so >= 5 * 15s = 75s before this fires),
23
+ // reconcileSurfaceHealth() stops auto-restarting the surface and latches it
24
+ // into "needs_help" instead of retrying forever. Without a cap, a surface
25
+ // that can never come up (a missing dependency, a bad config) gets
26
+ // relaunched every cooldown window FOREVER: the restart command itself can
27
+ // exit 0 (pm2 accepted it) while the process crashes immediately after, so
28
+ // this loop never sees a hard failure to stop on, and pm2's OWN crash-loop
29
+ // backoff never engages either because clauth's periodic explicit restart
30
+ // keeps resetting it. Root cause of the regen-media-local window-flashing
31
+ // incident (2026-09-05): a missing `tsx` dependency meant every restart
32
+ // "succeeded" and immediately crashed, forever, because nothing was
33
+ // counting. A latched surface self-heals the moment its health probe
34
+ // succeeds (handled before this cap is ever consulted) or a human/agent
35
+ // issues an explicit non-reconcile start/stop/restart (see runSurfaceAction).
36
+ export const MAX_CONSECUTIVE_RECONCILE_FAILURES = 5;
21
37
  const DOCUMENTATION_FIELDS = ["architecture", "operator_guide", "install", "runbook", "tool_reference", "release", "agent_context"];
22
38
 
23
39
  function hasMcpToken(value) {
@@ -182,11 +198,25 @@ export async function reconcileSurfaceHealth({ fetchImpl = globalThis.fetch, tim
182
198
  const error = health.error;
183
199
 
184
200
  if (healthy) {
185
- updateSurfaceState(id, { state: "current", last_health_at: observedAt, last_health_ok: true, last_health_error: null });
201
+ updateSurfaceState(id, { state: "current", last_health_at: observedAt, last_health_ok: true, last_health_error: null, consecutive_reconcile_failures: 0 });
186
202
  inspected.push({ surface_id: id, state: "healthy", observed_at: observedAt });
187
203
  continue;
188
204
  }
189
205
 
206
+ // Latched: this surface already exhausted MAX_CONSECUTIVE_RECONCILE_FAILURES
207
+ // and clauth stopped auto-restarting it. Record the failed probe (so the
208
+ // UI's "last checked" freshness stays honest) WITHOUT calling
209
+ // runSurfaceAction again — that call is exactly the crash loop this latch
210
+ // exists to stop. The only ways out are a health probe that succeeds
211
+ // (the branch above, checked every tick regardless of latch state) or an
212
+ // explicit human/agent action (runSurfaceAction resets the counter and
213
+ // clears this latch on any successful non-reconcile command).
214
+ if (surface.state === "needs_help") {
215
+ updateSurfaceState(id, { last_health_at: observedAt, last_health_ok: false, last_health_error: error });
216
+ inspected.push({ surface_id: id, state: "needs_help", error, latched: true, observed_at: observedAt });
217
+ continue;
218
+ }
219
+
190
220
  const lastAttempt = Date.parse(surface.last_reconcile_at || "") || 0;
191
221
  if (Date.now() - lastAttempt < HEALTH_RECONCILE_COOLDOWN_MS) {
192
222
  updateSurfaceState(id, { state: "unavailable", last_health_at: observedAt, last_health_ok: false, last_health_error: error });
@@ -206,15 +236,52 @@ export async function reconcileSurfaceHealth({ fetchImpl = globalThis.fetch, tim
206
236
  const commandCompleted = receipt?.resulting_state?.ok === true;
207
237
  const postHealth = commandCompleted ? await probeSurfaceHealth(url, fetchImpl, timeoutMs) : { healthy: false, error: receipt?.resulting_state?.state || "reconcile_failed" };
208
238
  const repaired = commandCompleted && postHealth.healthy;
239
+
240
+ if (repaired) {
241
+ updateSurfaceState(id, {
242
+ state: "current",
243
+ last_health_at: now(),
244
+ last_health_ok: true,
245
+ last_health_error: null,
246
+ last_reconcile_operation_id: receipt?.operationId || null,
247
+ consecutive_reconcile_failures: 0,
248
+ });
249
+ appendSupervisorEvent({ kind: "surface_reconciled", surface_id: id, operation_id: receipt?.operationId || null, command_completed: commandCompleted, health_ok: true, error: null });
250
+ inspected.push({ surface_id: id, state: "reconciled", error: null, operation_id: receipt?.operationId || null, observed_at: observedAt });
251
+ continue;
252
+ }
253
+
254
+ // Not repaired: count it. MAX_CONSECUTIVE_RECONCILE_FAILURES is the
255
+ // circuit breaker — cross it and the surface latches to "needs_help"
256
+ // instead of feeding another attempt back into runSurfaceAction next tick.
257
+ const failures = (surface.consecutive_reconcile_failures || 0) + 1;
258
+ const exhausted = failures >= MAX_CONSECUTIVE_RECONCILE_FAILURES;
209
259
  updateSurfaceState(id, {
210
- state: repaired ? "current" : "unavailable",
260
+ state: exhausted ? "needs_help" : "unavailable",
211
261
  last_health_at: now(),
212
- last_health_ok: repaired,
213
- last_health_error: repaired ? null : postHealth.error,
262
+ last_health_ok: false,
263
+ last_health_error: postHealth.error || error,
214
264
  last_reconcile_operation_id: receipt?.operationId || null,
265
+ consecutive_reconcile_failures: failures,
266
+ });
267
+ appendSupervisorEvent({
268
+ kind: exhausted ? "surface_needs_help" : "surface_reconcile_failed",
269
+ surface_id: id,
270
+ operation_id: receipt?.operationId || null,
271
+ command_completed: commandCompleted,
272
+ health_ok: false,
273
+ error: postHealth.error || error,
274
+ consecutive_failures: failures,
275
+ ...(exhausted ? { message: `${id} failed to come up after ${failures} consecutive restart attempts — clauth has stopped auto-restarting it. Check its log, fix the underlying cause, then Start/Restart it manually (dashboard or \`clauth mcp restart ${id}\`) to resume auto-repair.` } : {}),
276
+ });
277
+ inspected.push({
278
+ surface_id: id,
279
+ state: exhausted ? "needs_help" : "reconcile_failed",
280
+ error: postHealth.error || error,
281
+ operation_id: receipt?.operationId || null,
282
+ consecutive_failures: failures,
283
+ observed_at: observedAt,
215
284
  });
216
- appendSupervisorEvent({ kind: repaired ? "surface_reconciled" : "surface_reconcile_failed", surface_id: id, operation_id: receipt?.operationId || null, command_completed: commandCompleted, health_ok: postHealth.healthy, error: postHealth.error || null });
217
- inspected.push({ surface_id: id, state: repaired ? "reconciled" : "reconcile_failed", error: postHealth.error || error, operation_id: receipt?.operationId || null, observed_at: observedAt });
218
285
  }
219
286
  return { inspected };
220
287
  }
@@ -1245,6 +1312,22 @@ export function runSurfaceAction(id, action, actor = "localhost") {
1245
1312
  fallbackUsed = true;
1246
1313
  result.stderr = `restart exited ${restartStatus}; start fallback attempted\n${result.stderr || ""}`;
1247
1314
  }
1315
+ // A deliberate, successful manual action — start/stop/restart, never
1316
+ // "reconcile" itself — is a human or agent saying "I looked at this."
1317
+ // Give the surface a fresh crash-loop budget instead of carrying forward a
1318
+ // counter (or a "needs_help" latch) from before the intervention. Only
1319
+ // reconcileSurfaceHealth's own automatic attempts count against
1320
+ // MAX_CONSECUTIVE_RECONCILE_FAILURES, so this is the other side of that
1321
+ // cap: the way OUT of "needs_help", not just the way IN. Reset does not
1322
+ // claim the surface is healthy again — it has done no health probe — it
1323
+ // only clears the latch so the next reconcile tick (or health probe) gets
1324
+ // to judge it fresh.
1325
+ if (action !== "reconcile" && result.status === 0) {
1326
+ updateSurfaceState(id, {
1327
+ consecutive_reconcile_failures: 0,
1328
+ state: surface.state === "needs_help" ? "unavailable" : surface.state,
1329
+ });
1330
+ }
1248
1331
  return operation(action, { surface_id: id }, surface, {
1249
1332
  ok: result.status === 0,
1250
1333
  state: result.status === 0 ? "operation_completed" : "operation_failed",
@@ -12,8 +12,10 @@ import {
12
12
  isMcpServerPlugin,
13
13
  listPlugins,
14
14
  listSurfaces,
15
+ MAX_CONSECUTIVE_RECONCILE_FAILURES,
15
16
  probeAllSurfaceHealth,
16
17
  surfaceOpenUrl,
18
+ readSupervisorEvents,
17
19
  reconcileSurfaceHealth,
18
20
  registerPlugin,
19
21
  runPluginAction,
@@ -60,6 +62,33 @@ async function withTempSupervisor(fn) {
60
62
  }
61
63
  }
62
64
 
65
+ // Fast-forwards Date.now()/`new Date()` without real sleeping.
66
+ // HEALTH_RECONCILE_COOLDOWN_MS is a real 15s per attempt, and proving the
67
+ // crash-loop cap needs MAX_CONSECUTIVE_RECONCILE_FAILURES of them -- a test
68
+ // cannot afford to actually sleep that long. Overriding ONLY Date.now() is
69
+ // not enough: now() (supervisor-registry.js) stamps last_reconcile_at via
70
+ // `new Date().toISOString()`, so the cooldown gate (Date.now() minus
71
+ // Date.parse(last_reconcile_at)) would compare a mocked "now" against a REAL
72
+ // timestamp and never agree. Subclassing keeps every other Date behavior
73
+ // (Date.parse, `new Date(iso)`) real.
74
+ async function withMockedClock(fn) {
75
+ const RealDate = global.Date;
76
+ let clock = RealDate.now();
77
+ class MockDate extends RealDate {
78
+ constructor(...args) {
79
+ if (args.length === 0) super(clock);
80
+ else super(...args);
81
+ }
82
+ static now() { return clock; }
83
+ }
84
+ global.Date = MockDate;
85
+ try {
86
+ return await fn({ advance: (ms) => { clock += ms; } });
87
+ } finally {
88
+ global.Date = RealDate;
89
+ }
90
+ }
91
+
63
92
  function writePlugin(root, source, id, manifest) {
64
93
  const dir = path.join(root, source, id);
65
94
  fs.mkdirSync(dir, { recursive: true });
@@ -453,6 +482,135 @@ test("health reconciliation does not claim current when a successful command lea
453
482
  }
454
483
  });
455
484
 
485
+ test("a crash-looping surface stops auto-restarting after MAX_CONSECUTIVE_RECONCILE_FAILURES and needs a human", async () => {
486
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-crashloop-"));
487
+ const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
488
+ const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
489
+ const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
490
+ process.env.CLAUTH_SUPERVISOR_DIR = root;
491
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
492
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
493
+ try {
494
+ // restart "succeeds" (exit 0 -- pm2 would accept it) but health NEVER
495
+ // recovers: exactly the regen-media-local shape -- a process that
496
+ // restarts cleanly and crashes immediately after, forever.
497
+ writePlugin(root, "managed", "crashloop-demo", baseManifest("crashloop-demo", {
498
+ core: true,
499
+ enable_default: true,
500
+ surfaces: [{
501
+ id: "primary",
502
+ destination: "local/clauth/pm2",
503
+ lifecycle_owner: "clauth",
504
+ port: 39121,
505
+ health: "/health",
506
+ restart: [process.execPath, "--version"],
507
+ start: [process.execPath, "--version"],
508
+ }],
509
+ }));
510
+ discoverPlugins();
511
+ const alwaysDown = { fetchImpl: async () => ({ ok: false, status: 503 }) };
512
+
513
+ await withMockedClock(async ({ advance }) => {
514
+ let last;
515
+ for (let i = 0; i < MAX_CONSECUTIVE_RECONCILE_FAILURES; i++) {
516
+ advance(20000); // past the 15s reconcile cooldown every tick
517
+ last = await reconcileSurfaceHealth(alwaysDown);
518
+ }
519
+ const surface = listSurfaces().find((item) => item.plugin_id === "crashloop-demo");
520
+ assert.equal(surface.state, "needs_help", "must latch once the cap is reached");
521
+ assert.equal(surface.consecutive_reconcile_failures, MAX_CONSECUTIVE_RECONCILE_FAILURES);
522
+ assert.equal(last.inspected[0].state, "needs_help");
523
+
524
+ // THE load-bearing assertion: one more tick past cooldown must NOT
525
+ // spawn another restart. Count "reconcile" operation receipts before
526
+ // and after -- if the count moves, the loop never actually stopped.
527
+ const before = readSupervisorEvents(1000).filter((e) => e.kind === "operation" && e.action === "reconcile").length;
528
+ advance(20000);
529
+ const again = await reconcileSurfaceHealth(alwaysDown);
530
+ const after = readSupervisorEvents(1000).filter((e) => e.kind === "operation" && e.action === "reconcile").length;
531
+ assert.equal(after, before, "a latched surface must not be re-restarted -- that is the crash loop this cap exists to stop");
532
+ assert.equal(again.inspected[0].state, "needs_help");
533
+ assert.equal(again.inspected[0].latched, true);
534
+
535
+ // Self-heal: health recovering on its own (independent of clauth's
536
+ // restart) clears the latch -- the cap is a circuit breaker, not a
537
+ // permanent ban.
538
+ advance(20000);
539
+ const healed = await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: true, status: 200 }) });
540
+ assert.equal(healed.inspected[0].state, "healthy");
541
+ const healedSurface = listSurfaces().find((item) => item.plugin_id === "crashloop-demo");
542
+ assert.equal(healedSurface.state, "current");
543
+ assert.equal(healedSurface.consecutive_reconcile_failures, 0);
544
+ });
545
+ } finally {
546
+ if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
547
+ else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
548
+ if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
549
+ else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
550
+ if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
551
+ else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
552
+ fs.rmSync(root, { recursive: true, force: true });
553
+ }
554
+ });
555
+
556
+ test("an explicit manual restart clears a needs_help latch and gives a fresh crash-loop budget", async () => {
557
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-manual-reset-"));
558
+ const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
559
+ const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
560
+ const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
561
+ process.env.CLAUTH_SUPERVISOR_DIR = root;
562
+ process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
563
+ process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
564
+ try {
565
+ writePlugin(root, "managed", "manual-reset-demo", baseManifest("manual-reset-demo", {
566
+ core: true,
567
+ enable_default: true,
568
+ surfaces: [{
569
+ id: "primary",
570
+ destination: "local/clauth/pm2",
571
+ lifecycle_owner: "clauth",
572
+ port: 39122,
573
+ health: "/health",
574
+ restart: [process.execPath, "--version"],
575
+ start: [process.execPath, "--version"],
576
+ }],
577
+ }));
578
+ discoverPlugins();
579
+ const alwaysDown = { fetchImpl: async () => ({ ok: false, status: 503 }) };
580
+
581
+ await withMockedClock(async ({ advance }) => {
582
+ for (let i = 0; i < MAX_CONSECUTIVE_RECONCILE_FAILURES; i++) {
583
+ advance(20000);
584
+ await reconcileSurfaceHealth(alwaysDown);
585
+ }
586
+ assert.equal(listSurfaces()[0].state, "needs_help");
587
+
588
+ // A human/agent looks at it and issues an explicit restart -- through
589
+ // runSurfaceAction directly, never through the "reconcile" action the
590
+ // background loop uses.
591
+ const receipt = runSurfaceAction("manual-reset-demo:primary", "restart", "human");
592
+ assert.equal(receipt.resulting_state.ok, true);
593
+ const reset = listSurfaces()[0];
594
+ assert.equal(reset.state, "unavailable", "cleared out of needs_help, but not falsely claimed healthy without a probe");
595
+ assert.equal(reset.consecutive_reconcile_failures, 0);
596
+
597
+ // And the surface gets its full budget back, not zero.
598
+ advance(20000);
599
+ const tick = await reconcileSurfaceHealth(alwaysDown);
600
+ assert.equal(tick.inspected[0].state, "reconcile_failed");
601
+ assert.equal(listSurfaces()[0].consecutive_reconcile_failures, 1);
602
+ });
603
+ } finally {
604
+ if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
605
+ else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
606
+ if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
607
+ else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
608
+ if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
609
+ else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
610
+ fs.rmSync(root, { recursive: true, force: true });
611
+ }
612
+ });
613
+
456
614
  test("health reconciliation never restarts external or plugin-owned surfaces", async () => {
457
615
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-observe-"));
458
616
  const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "2.15.5",
3
+ "version": "2.15.6",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {