lensmcp 1.16.29 → 1.16.31
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/bundled/bridge.js +75 -2
- package/bundled/capture-runner.js +45 -12
- package/bundled/dashboard.js +71 -6
- package/bundled/main.js +57 -4
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +283 -22
- package/lib/sweep.d.ts +12 -0
- package/lib/sweep.d.ts.map +1 -0
- package/lib/sweep.js +60 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
package/bundled/bridge.js
CHANGED
|
@@ -3733,7 +3733,74 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
3733
3733
|
// libs/bridge/src/lib/env-sink.ts
|
|
3734
3734
|
import { connect } from "node:net";
|
|
3735
3735
|
import { createSocket } from "node:dgram";
|
|
3736
|
-
import { appendFileSync } from "node:fs";
|
|
3736
|
+
import { appendFileSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
3737
|
+
var MAX_EVENT_BYTES = (() => {
|
|
3738
|
+
const n = Number(process.env["LENSMCP_EVENT_MAX_BYTES"]);
|
|
3739
|
+
return Number.isFinite(n) && n > 0 ? n : 50 * 1024 * 1024;
|
|
3740
|
+
})();
|
|
3741
|
+
var MAX_EVENT_FILES = (() => {
|
|
3742
|
+
const n = Number(process.env["LENSMCP_EVENT_MAX_FILES"]);
|
|
3743
|
+
return Number.isFinite(n) && n >= 1 && n <= 20 ? Math.floor(n) : 5;
|
|
3744
|
+
})();
|
|
3745
|
+
var ROTATE_CHECK_BYTES = 1024 * 1024;
|
|
3746
|
+
var bytesSinceCheck = 0;
|
|
3747
|
+
function rotateIfLarge(file, addedBytes) {
|
|
3748
|
+
bytesSinceCheck += addedBytes;
|
|
3749
|
+
if (bytesSinceCheck < ROTATE_CHECK_BYTES) return;
|
|
3750
|
+
bytesSinceCheck = 0;
|
|
3751
|
+
try {
|
|
3752
|
+
if (statSync(file).size <= MAX_EVENT_BYTES) return;
|
|
3753
|
+
} catch {
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
try {
|
|
3757
|
+
unlinkSync(`${file}.${MAX_EVENT_FILES}`);
|
|
3758
|
+
} catch {
|
|
3759
|
+
}
|
|
3760
|
+
for (let gen = MAX_EVENT_FILES - 1; gen >= 1; gen--) {
|
|
3761
|
+
try {
|
|
3762
|
+
renameSync(`${file}.${gen}`, `${file}.${gen + 1}`);
|
|
3763
|
+
} catch {
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
try {
|
|
3767
|
+
renameSync(file, `${file}.1`);
|
|
3768
|
+
} catch {
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
var FLOOD_PER_SEC = (() => {
|
|
3772
|
+
const raw = process.env["LENSMCP_EVENT_MAX_EPS"];
|
|
3773
|
+
if (raw === "off") return Infinity;
|
|
3774
|
+
const n = Number(raw);
|
|
3775
|
+
if (raw !== void 0 && raw !== "" && Number.isFinite(n)) return n <= 0 ? Infinity : n;
|
|
3776
|
+
return 5;
|
|
3777
|
+
})();
|
|
3778
|
+
var FLOOD_BURST = Math.max(30, FLOOD_PER_SEC === Infinity ? 0 : FLOOD_PER_SEC * 4);
|
|
3779
|
+
var floodBuckets = /* @__PURE__ */ new Map();
|
|
3780
|
+
function floodAdmit(fingerprint2) {
|
|
3781
|
+
if (FLOOD_PER_SEC === Infinity) return 0;
|
|
3782
|
+
const now = Date.now();
|
|
3783
|
+
let b = floodBuckets.get(fingerprint2);
|
|
3784
|
+
if (!b) {
|
|
3785
|
+
if (floodBuckets.size >= 2048) {
|
|
3786
|
+
for (const [k, v] of floodBuckets) if (now - v.lastSeenAt > 6e4) floodBuckets.delete(k);
|
|
3787
|
+
if (floodBuckets.size >= 2048) floodBuckets.clear();
|
|
3788
|
+
}
|
|
3789
|
+
b = { tokens: FLOOD_BURST, lastRefillAt: now, suppressed: 0, lastSeenAt: now };
|
|
3790
|
+
floodBuckets.set(fingerprint2, b);
|
|
3791
|
+
}
|
|
3792
|
+
b.lastSeenAt = now;
|
|
3793
|
+
b.tokens = Math.min(FLOOD_BURST, b.tokens + (now - b.lastRefillAt) / 1e3 * FLOOD_PER_SEC);
|
|
3794
|
+
b.lastRefillAt = now;
|
|
3795
|
+
if (b.tokens >= 1) {
|
|
3796
|
+
b.tokens -= 1;
|
|
3797
|
+
const suppressed = b.suppressed;
|
|
3798
|
+
b.suppressed = 0;
|
|
3799
|
+
return suppressed;
|
|
3800
|
+
}
|
|
3801
|
+
b.suppressed += 1;
|
|
3802
|
+
return -1;
|
|
3803
|
+
}
|
|
3737
3804
|
var cachedEnvSink;
|
|
3738
3805
|
function resolveEnvSink() {
|
|
3739
3806
|
if (cachedEnvSink !== void 0) return cachedEnvSink;
|
|
@@ -3741,7 +3808,13 @@ function resolveEnvSink() {
|
|
|
3741
3808
|
if (filePath) {
|
|
3742
3809
|
cachedEnvSink = (event) => {
|
|
3743
3810
|
try {
|
|
3744
|
-
|
|
3811
|
+
const suppressed = floodAdmit(event.fingerprint);
|
|
3812
|
+
if (suppressed < 0) return;
|
|
3813
|
+
const line = JSON.stringify(
|
|
3814
|
+
suppressed > 0 ? { ...event, raw: { ...event.raw ?? {}, floodSuppressed: suppressed } } : event
|
|
3815
|
+
) + "\n";
|
|
3816
|
+
rotateIfLarge(filePath, line.length);
|
|
3817
|
+
appendFileSync(filePath, line);
|
|
3745
3818
|
} catch {
|
|
3746
3819
|
}
|
|
3747
3820
|
};
|
|
@@ -33558,7 +33558,7 @@ var require_chrome_remote_interface = __commonJS({
|
|
|
33558
33558
|
});
|
|
33559
33559
|
|
|
33560
33560
|
// libs/browser-capture/src/capture-runner.ts
|
|
33561
|
-
import { appendFileSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
33561
|
+
import { appendFileSync, readFileSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
33562
33562
|
|
|
33563
33563
|
// libs/core/dist/lib/ulid.js
|
|
33564
33564
|
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
@@ -33826,6 +33826,7 @@ function planOrphanReap(psLines, isOwnerAlive, selfPid) {
|
|
|
33826
33826
|
function reapOrphanCaptureChrome(log) {
|
|
33827
33827
|
if (process.platform === "win32")
|
|
33828
33828
|
return 0;
|
|
33829
|
+
let reaped = 0;
|
|
33829
33830
|
let out;
|
|
33830
33831
|
try {
|
|
33831
33832
|
out = execFileSync2("ps", ["ax", "-ww", "-o", "pid=,command="], {
|
|
@@ -33834,20 +33835,33 @@ function reapOrphanCaptureChrome(log) {
|
|
|
33834
33835
|
maxBuffer: 64 * 1024 * 1024
|
|
33835
33836
|
});
|
|
33836
33837
|
} catch {
|
|
33837
|
-
return 0;
|
|
33838
33838
|
}
|
|
33839
|
-
|
|
33840
|
-
|
|
33841
|
-
|
|
33842
|
-
|
|
33843
|
-
|
|
33844
|
-
|
|
33845
|
-
|
|
33839
|
+
if (out !== void 0) {
|
|
33840
|
+
for (const orphan of planOrphanReap(out.split("\n"), isAlive, process.pid)) {
|
|
33841
|
+
try {
|
|
33842
|
+
process.kill(orphan.pid, "SIGKILL");
|
|
33843
|
+
reaped += 1;
|
|
33844
|
+
log?.(`[browser-capture] reaped orphaned capture Chrome pid=${orphan.pid} (owner ${orphan.ownerPid} is gone)`);
|
|
33845
|
+
} catch {
|
|
33846
|
+
}
|
|
33847
|
+
removeUserDataDir(orphan.userDataDir);
|
|
33846
33848
|
}
|
|
33847
|
-
removeUserDataDir(orphan.userDataDir);
|
|
33848
33849
|
}
|
|
33850
|
+
sweepStaleMarkerDirs();
|
|
33849
33851
|
return reaped;
|
|
33850
33852
|
}
|
|
33853
|
+
function sweepStaleMarkerDirs() {
|
|
33854
|
+
const ownerRe = new RegExp(`^${CAPTURE_DIR_MARKER}(\\d+)-`);
|
|
33855
|
+
try {
|
|
33856
|
+
for (const entry of readdirSync(tmpdir())) {
|
|
33857
|
+
const owner = Number(ownerRe.exec(entry)?.[1]);
|
|
33858
|
+
if (!Number.isFinite(owner) || owner === process.pid || isAlive(owner))
|
|
33859
|
+
continue;
|
|
33860
|
+
removeUserDataDir(join2(tmpdir(), entry));
|
|
33861
|
+
}
|
|
33862
|
+
} catch {
|
|
33863
|
+
}
|
|
33864
|
+
}
|
|
33851
33865
|
function isAlive(pid) {
|
|
33852
33866
|
try {
|
|
33853
33867
|
process.kill(pid, 0);
|
|
@@ -34883,7 +34897,11 @@ reapOrphanCaptureChrome((m) => console.error(m));
|
|
|
34883
34897
|
setInterval(() => reapOrphanCaptureChrome((m) => console.error(m)), 6e4).unref();
|
|
34884
34898
|
var MAX_EVENT_BYTES = (() => {
|
|
34885
34899
|
const n = Number(process.env["LENSMCP_EVENT_MAX_BYTES"]);
|
|
34886
|
-
return Number.isFinite(n) && n > 0 ? n :
|
|
34900
|
+
return Number.isFinite(n) && n > 0 ? n : 50 * 1024 * 1024;
|
|
34901
|
+
})();
|
|
34902
|
+
var MAX_EVENT_FILES = (() => {
|
|
34903
|
+
const n = Number(process.env["LENSMCP_EVENT_MAX_FILES"]);
|
|
34904
|
+
return Number.isFinite(n) && n >= 1 && n <= 20 ? Math.floor(n) : 5;
|
|
34887
34905
|
})();
|
|
34888
34906
|
var ROTATE_CHECK_BYTES = 1024 * 1024;
|
|
34889
34907
|
var bytesSinceCheck = 0;
|
|
@@ -34892,7 +34910,22 @@ function rotateIfLarge(file, lineBytes) {
|
|
|
34892
34910
|
if (bytesSinceCheck < ROTATE_CHECK_BYTES) return;
|
|
34893
34911
|
bytesSinceCheck = 0;
|
|
34894
34912
|
try {
|
|
34895
|
-
if (statSync(file).size
|
|
34913
|
+
if (statSync(file).size <= MAX_EVENT_BYTES) return;
|
|
34914
|
+
} catch {
|
|
34915
|
+
return;
|
|
34916
|
+
}
|
|
34917
|
+
try {
|
|
34918
|
+
unlinkSync(`${file}.${MAX_EVENT_FILES}`);
|
|
34919
|
+
} catch {
|
|
34920
|
+
}
|
|
34921
|
+
for (let gen = MAX_EVENT_FILES - 1; gen >= 1; gen--) {
|
|
34922
|
+
try {
|
|
34923
|
+
renameSync(`${file}.${gen}`, `${file}.${gen + 1}`);
|
|
34924
|
+
} catch {
|
|
34925
|
+
}
|
|
34926
|
+
}
|
|
34927
|
+
try {
|
|
34928
|
+
renameSync(file, `${file}.1`);
|
|
34896
34929
|
} catch {
|
|
34897
34930
|
}
|
|
34898
34931
|
}
|
package/bundled/dashboard.js
CHANGED
|
@@ -6,7 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
|
|
7
7
|
// servers/lensmcp-mcp/dist/dashboard.js
|
|
8
8
|
import "reflect-metadata";
|
|
9
|
-
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
9
|
+
import { closeSync as closeSync2, openSync as openSync2, readdirSync, readFileSync as readFileSync2, readSync as readSync2, statSync as statSync2 } from "node:fs";
|
|
10
10
|
import { createServer as createServer2 } from "node:http";
|
|
11
11
|
|
|
12
12
|
// libs/core/dist/lib/ulid.js
|
|
@@ -352,6 +352,10 @@ function compileStory(events, options = {}) {
|
|
|
352
352
|
if (pool.length === 0)
|
|
353
353
|
return void 0;
|
|
354
354
|
pool.sort((a, b) => a.timestamp - b.timestamp);
|
|
355
|
+
const MAX_STORY_EVENTS = 1e3;
|
|
356
|
+
if (pool.length > MAX_STORY_EVENTS) {
|
|
357
|
+
pool = [pool[0], ...pool.slice(pool.length - (MAX_STORY_EVENTS - 1))];
|
|
358
|
+
}
|
|
355
359
|
const origin = pool[0];
|
|
356
360
|
const phases = [];
|
|
357
361
|
let finalRender;
|
|
@@ -16428,6 +16432,7 @@ var URI_CURRENT = "flow://current";
|
|
|
16428
16432
|
var URI_ORIGINS = "flow://origins/recent";
|
|
16429
16433
|
var MAX_FLOWS = 50;
|
|
16430
16434
|
var MAX_ORIGINS = 50;
|
|
16435
|
+
var MAX_EVENTS_PER_FLOW = 2e3;
|
|
16431
16436
|
function installFlowReducer(args) {
|
|
16432
16437
|
const state2 = {
|
|
16433
16438
|
flows: /* @__PURE__ */ new Map(),
|
|
@@ -16470,6 +16475,9 @@ function installFlowReducer(args) {
|
|
|
16470
16475
|
void args.resources.markDirty(URI_ORIGINS);
|
|
16471
16476
|
}
|
|
16472
16477
|
entry.events.push(event);
|
|
16478
|
+
if (entry.events.length > MAX_EVENTS_PER_FLOW) {
|
|
16479
|
+
entry.events.splice(0, entry.events.length - MAX_EVENTS_PER_FLOW);
|
|
16480
|
+
}
|
|
16473
16481
|
entry.summary.eventCount += 1;
|
|
16474
16482
|
entry.summary.lastSeenAt = event.timestamp;
|
|
16475
16483
|
const idx = state2.recentOrder.indexOf(flowId);
|
|
@@ -17677,6 +17685,7 @@ NestApp = __decorate([
|
|
|
17677
17685
|
var URI_PROVIDERS = "nest://providers";
|
|
17678
17686
|
var URI_REQUESTS = "nest://requests/recent";
|
|
17679
17687
|
var MAX_RECENT2 = 50;
|
|
17688
|
+
var MAX_PROVIDERS = 500;
|
|
17680
17689
|
function installNestReducer(args) {
|
|
17681
17690
|
const state2 = { providers: /* @__PURE__ */ new Map(), recentRequests: [] };
|
|
17682
17691
|
const handler = (event) => {
|
|
@@ -17685,7 +17694,15 @@ function installNestReducer(args) {
|
|
|
17685
17694
|
const raw = event.raw ?? null;
|
|
17686
17695
|
if (raw?.kind === "singleton-instance" && raw.provider) {
|
|
17687
17696
|
const p = raw.provider;
|
|
17697
|
+
state2.providers.delete(p.instanceId);
|
|
17688
17698
|
state2.providers.set(p.instanceId, { ...p });
|
|
17699
|
+
if (state2.providers.size > MAX_PROVIDERS) {
|
|
17700
|
+
for (const k of state2.providers.keys()) {
|
|
17701
|
+
state2.providers.delete(k);
|
|
17702
|
+
if (state2.providers.size <= MAX_PROVIDERS)
|
|
17703
|
+
break;
|
|
17704
|
+
}
|
|
17705
|
+
}
|
|
17689
17706
|
void args.resources.markDirty(URI_PROVIDERS);
|
|
17690
17707
|
return;
|
|
17691
17708
|
}
|
|
@@ -18582,6 +18599,7 @@ var URI_MEMOS_RECOMPUTED = "react://memos/recomputed";
|
|
|
18582
18599
|
var URI_HOOKS_RECENT = "react://hooks/recent";
|
|
18583
18600
|
var MAX_RECENT4 = 100;
|
|
18584
18601
|
var MAX_SLOW2 = 50;
|
|
18602
|
+
var MAX_INSTANCES = 500;
|
|
18585
18603
|
function installReactReducer(args) {
|
|
18586
18604
|
const slowRenderMs = resolveThresholds(args.thresholds).slowRenderMs;
|
|
18587
18605
|
const state2 = {
|
|
@@ -18605,7 +18623,15 @@ function installReactReducer(args) {
|
|
|
18605
18623
|
if (!r)
|
|
18606
18624
|
return;
|
|
18607
18625
|
pushBounded3(state2.rendersRecent, r, MAX_RECENT4);
|
|
18626
|
+
state2.rendersByInstance.delete(r.componentInstanceId);
|
|
18608
18627
|
state2.rendersByInstance.set(r.componentInstanceId, r);
|
|
18628
|
+
if (state2.rendersByInstance.size > MAX_INSTANCES) {
|
|
18629
|
+
for (const k of state2.rendersByInstance.keys()) {
|
|
18630
|
+
state2.rendersByInstance.delete(k);
|
|
18631
|
+
if (state2.rendersByInstance.size <= MAX_INSTANCES)
|
|
18632
|
+
break;
|
|
18633
|
+
}
|
|
18634
|
+
}
|
|
18609
18635
|
if (r.actualDurationMs >= slowRenderMs) {
|
|
18610
18636
|
pushBounded3(state2.rendersSlow, r, MAX_SLOW2);
|
|
18611
18637
|
state2.rendersSlow.sort((a, b) => b.actualDurationMs - a.actualDurationMs);
|
|
@@ -18836,6 +18862,7 @@ var URI_UPDATES = "valtio://updates/recent";
|
|
|
18836
18862
|
var URI_STORES = "valtio://stores";
|
|
18837
18863
|
var URI_SUBSCRIBERS = "valtio://subscribers";
|
|
18838
18864
|
var MAX_UPDATES = 100;
|
|
18865
|
+
var MAX_PATHS = 1e3;
|
|
18839
18866
|
function installValtioReducer(args) {
|
|
18840
18867
|
const state2 = {
|
|
18841
18868
|
updates: [],
|
|
@@ -18859,7 +18886,16 @@ function installValtioReducer(args) {
|
|
|
18859
18886
|
if (state2.updates.length > MAX_UPDATES) {
|
|
18860
18887
|
state2.updates.splice(0, state2.updates.length - MAX_UPDATES);
|
|
18861
18888
|
}
|
|
18862
|
-
|
|
18889
|
+
const pathKey = `${u.storeId}::${u.path}`;
|
|
18890
|
+
state2.latestUpdateByPath.delete(pathKey);
|
|
18891
|
+
state2.latestUpdateByPath.set(pathKey, rec);
|
|
18892
|
+
if (state2.latestUpdateByPath.size > MAX_PATHS) {
|
|
18893
|
+
for (const k of state2.latestUpdateByPath.keys()) {
|
|
18894
|
+
state2.latestUpdateByPath.delete(k);
|
|
18895
|
+
if (state2.latestUpdateByPath.size <= MAX_PATHS)
|
|
18896
|
+
break;
|
|
18897
|
+
}
|
|
18898
|
+
}
|
|
18863
18899
|
void args.resources.markDirty(URI_UPDATES);
|
|
18864
18900
|
return;
|
|
18865
18901
|
}
|
|
@@ -19547,8 +19583,25 @@ var URI_CURRENT7 = "memory://current";
|
|
|
19547
19583
|
var URI_OWNERS = "memory://owners";
|
|
19548
19584
|
var URI_GROWING = "memory://owners/growing";
|
|
19549
19585
|
var URI_LEAKS = "memory://leaks/suspected";
|
|
19586
|
+
var MAX_LEAKS = 200;
|
|
19587
|
+
var MAX_FLOWIDS_PER_OWNER = 50;
|
|
19550
19588
|
function installMemoryReducer(args) {
|
|
19551
19589
|
const state2 = { owners: /* @__PURE__ */ new Map(), leaks: [] };
|
|
19590
|
+
const pushLeak = (leak) => {
|
|
19591
|
+
state2.leaks.push(leak);
|
|
19592
|
+
if (state2.leaks.length > MAX_LEAKS)
|
|
19593
|
+
state2.leaks.splice(0, state2.leaks.length - MAX_LEAKS);
|
|
19594
|
+
};
|
|
19595
|
+
const noteFlow = (o, flowId) => {
|
|
19596
|
+
o.flowIds.add(flowId);
|
|
19597
|
+
if (o.flowIds.size > MAX_FLOWIDS_PER_OWNER) {
|
|
19598
|
+
for (const id of o.flowIds) {
|
|
19599
|
+
o.flowIds.delete(id);
|
|
19600
|
+
if (o.flowIds.size <= MAX_FLOWIDS_PER_OWNER)
|
|
19601
|
+
break;
|
|
19602
|
+
}
|
|
19603
|
+
}
|
|
19604
|
+
};
|
|
19552
19605
|
const ensure = (ownerId) => {
|
|
19553
19606
|
let o = state2.owners.get(ownerId);
|
|
19554
19607
|
if (!o) {
|
|
@@ -19579,7 +19632,7 @@ function installMemoryReducer(args) {
|
|
|
19579
19632
|
o.estimatedBytes = raw.mutation.afterCount * 64;
|
|
19580
19633
|
o.mutations += 1;
|
|
19581
19634
|
if (event.context.flowId)
|
|
19582
|
-
o
|
|
19635
|
+
noteFlow(o, event.context.flowId);
|
|
19583
19636
|
void args.resources.markDirty(URI_OWNERS);
|
|
19584
19637
|
void args.resources.markDirty(URI_GROWING);
|
|
19585
19638
|
return;
|
|
@@ -19587,7 +19640,7 @@ function installMemoryReducer(args) {
|
|
|
19587
19640
|
case "memory-retention": {
|
|
19588
19641
|
if (!raw.retention)
|
|
19589
19642
|
return;
|
|
19590
|
-
|
|
19643
|
+
pushLeak({
|
|
19591
19644
|
kind: "retention",
|
|
19592
19645
|
ownerId: raw.retention.ownerId,
|
|
19593
19646
|
flowId: raw.retention.flowId,
|
|
@@ -19600,7 +19653,7 @@ function installMemoryReducer(args) {
|
|
|
19600
19653
|
case "singleton-stale-generation": {
|
|
19601
19654
|
if (!raw.staleGeneration)
|
|
19602
19655
|
return;
|
|
19603
|
-
|
|
19656
|
+
pushLeak({
|
|
19604
19657
|
kind: "stale-generation",
|
|
19605
19658
|
logicalId: raw.staleGeneration.logicalId,
|
|
19606
19659
|
detail: { ...raw.staleGeneration },
|
|
@@ -20358,7 +20411,19 @@ session.bus.subscribe(appendLog);
|
|
|
20358
20411
|
(() => {
|
|
20359
20412
|
try {
|
|
20360
20413
|
const file2 = process.env["LENSMCP_EVENT_FILE"] ?? `${process.cwd()}/.lensmcp/events.jsonl`;
|
|
20361
|
-
const
|
|
20414
|
+
const REPLAY_BYTES = 8 * 1024 * 1024;
|
|
20415
|
+
const size = statSync2(file2).size;
|
|
20416
|
+
const start = Math.max(0, size - REPLAY_BYTES);
|
|
20417
|
+
const buf = Buffer.alloc(size - start);
|
|
20418
|
+
const fd = openSync2(file2, "r");
|
|
20419
|
+
try {
|
|
20420
|
+
readSync2(fd, buf, 0, buf.length, start);
|
|
20421
|
+
} finally {
|
|
20422
|
+
closeSync2(fd);
|
|
20423
|
+
}
|
|
20424
|
+
const lines = buf.toString("utf8").trim().split("\n").slice(-25e3);
|
|
20425
|
+
if (start > 0)
|
|
20426
|
+
lines.shift();
|
|
20362
20427
|
for (const line of lines) {
|
|
20363
20428
|
try {
|
|
20364
20429
|
const e = JSON.parse(line);
|
package/bundled/main.js
CHANGED
|
@@ -362,6 +362,10 @@ function compileStory(events, options = {}) {
|
|
|
362
362
|
if (pool.length === 0)
|
|
363
363
|
return void 0;
|
|
364
364
|
pool.sort((a, b) => a.timestamp - b.timestamp);
|
|
365
|
+
const MAX_STORY_EVENTS = 1e3;
|
|
366
|
+
if (pool.length > MAX_STORY_EVENTS) {
|
|
367
|
+
pool = [pool[0], ...pool.slice(pool.length - (MAX_STORY_EVENTS - 1))];
|
|
368
|
+
}
|
|
365
369
|
const origin = pool[0];
|
|
366
370
|
const phases = [];
|
|
367
371
|
let finalRender;
|
|
@@ -16430,6 +16434,7 @@ var URI_CURRENT = "flow://current";
|
|
|
16430
16434
|
var URI_ORIGINS = "flow://origins/recent";
|
|
16431
16435
|
var MAX_FLOWS = 50;
|
|
16432
16436
|
var MAX_ORIGINS = 50;
|
|
16437
|
+
var MAX_EVENTS_PER_FLOW = 2e3;
|
|
16433
16438
|
function installFlowReducer(args) {
|
|
16434
16439
|
const state2 = {
|
|
16435
16440
|
flows: /* @__PURE__ */ new Map(),
|
|
@@ -16472,6 +16477,9 @@ function installFlowReducer(args) {
|
|
|
16472
16477
|
void args.resources.markDirty(URI_ORIGINS);
|
|
16473
16478
|
}
|
|
16474
16479
|
entry.events.push(event);
|
|
16480
|
+
if (entry.events.length > MAX_EVENTS_PER_FLOW) {
|
|
16481
|
+
entry.events.splice(0, entry.events.length - MAX_EVENTS_PER_FLOW);
|
|
16482
|
+
}
|
|
16475
16483
|
entry.summary.eventCount += 1;
|
|
16476
16484
|
entry.summary.lastSeenAt = event.timestamp;
|
|
16477
16485
|
const idx = state2.recentOrder.indexOf(flowId);
|
|
@@ -17679,6 +17687,7 @@ NestApp = __decorate([
|
|
|
17679
17687
|
var URI_PROVIDERS = "nest://providers";
|
|
17680
17688
|
var URI_REQUESTS = "nest://requests/recent";
|
|
17681
17689
|
var MAX_RECENT2 = 50;
|
|
17690
|
+
var MAX_PROVIDERS = 500;
|
|
17682
17691
|
function installNestReducer(args) {
|
|
17683
17692
|
const state2 = { providers: /* @__PURE__ */ new Map(), recentRequests: [] };
|
|
17684
17693
|
const handler = (event) => {
|
|
@@ -17687,7 +17696,15 @@ function installNestReducer(args) {
|
|
|
17687
17696
|
const raw = event.raw ?? null;
|
|
17688
17697
|
if (raw?.kind === "singleton-instance" && raw.provider) {
|
|
17689
17698
|
const p = raw.provider;
|
|
17699
|
+
state2.providers.delete(p.instanceId);
|
|
17690
17700
|
state2.providers.set(p.instanceId, { ...p });
|
|
17701
|
+
if (state2.providers.size > MAX_PROVIDERS) {
|
|
17702
|
+
for (const k of state2.providers.keys()) {
|
|
17703
|
+
state2.providers.delete(k);
|
|
17704
|
+
if (state2.providers.size <= MAX_PROVIDERS)
|
|
17705
|
+
break;
|
|
17706
|
+
}
|
|
17707
|
+
}
|
|
17691
17708
|
void args.resources.markDirty(URI_PROVIDERS);
|
|
17692
17709
|
return;
|
|
17693
17710
|
}
|
|
@@ -18584,6 +18601,7 @@ var URI_MEMOS_RECOMPUTED = "react://memos/recomputed";
|
|
|
18584
18601
|
var URI_HOOKS_RECENT = "react://hooks/recent";
|
|
18585
18602
|
var MAX_RECENT4 = 100;
|
|
18586
18603
|
var MAX_SLOW2 = 50;
|
|
18604
|
+
var MAX_INSTANCES = 500;
|
|
18587
18605
|
function installReactReducer(args) {
|
|
18588
18606
|
const slowRenderMs = resolveThresholds(args.thresholds).slowRenderMs;
|
|
18589
18607
|
const state2 = {
|
|
@@ -18607,7 +18625,15 @@ function installReactReducer(args) {
|
|
|
18607
18625
|
if (!r)
|
|
18608
18626
|
return;
|
|
18609
18627
|
pushBounded3(state2.rendersRecent, r, MAX_RECENT4);
|
|
18628
|
+
state2.rendersByInstance.delete(r.componentInstanceId);
|
|
18610
18629
|
state2.rendersByInstance.set(r.componentInstanceId, r);
|
|
18630
|
+
if (state2.rendersByInstance.size > MAX_INSTANCES) {
|
|
18631
|
+
for (const k of state2.rendersByInstance.keys()) {
|
|
18632
|
+
state2.rendersByInstance.delete(k);
|
|
18633
|
+
if (state2.rendersByInstance.size <= MAX_INSTANCES)
|
|
18634
|
+
break;
|
|
18635
|
+
}
|
|
18636
|
+
}
|
|
18611
18637
|
if (r.actualDurationMs >= slowRenderMs) {
|
|
18612
18638
|
pushBounded3(state2.rendersSlow, r, MAX_SLOW2);
|
|
18613
18639
|
state2.rendersSlow.sort((a, b) => b.actualDurationMs - a.actualDurationMs);
|
|
@@ -18838,6 +18864,7 @@ var URI_UPDATES = "valtio://updates/recent";
|
|
|
18838
18864
|
var URI_STORES = "valtio://stores";
|
|
18839
18865
|
var URI_SUBSCRIBERS = "valtio://subscribers";
|
|
18840
18866
|
var MAX_UPDATES = 100;
|
|
18867
|
+
var MAX_PATHS = 1e3;
|
|
18841
18868
|
function installValtioReducer(args) {
|
|
18842
18869
|
const state2 = {
|
|
18843
18870
|
updates: [],
|
|
@@ -18861,7 +18888,16 @@ function installValtioReducer(args) {
|
|
|
18861
18888
|
if (state2.updates.length > MAX_UPDATES) {
|
|
18862
18889
|
state2.updates.splice(0, state2.updates.length - MAX_UPDATES);
|
|
18863
18890
|
}
|
|
18864
|
-
|
|
18891
|
+
const pathKey = `${u.storeId}::${u.path}`;
|
|
18892
|
+
state2.latestUpdateByPath.delete(pathKey);
|
|
18893
|
+
state2.latestUpdateByPath.set(pathKey, rec);
|
|
18894
|
+
if (state2.latestUpdateByPath.size > MAX_PATHS) {
|
|
18895
|
+
for (const k of state2.latestUpdateByPath.keys()) {
|
|
18896
|
+
state2.latestUpdateByPath.delete(k);
|
|
18897
|
+
if (state2.latestUpdateByPath.size <= MAX_PATHS)
|
|
18898
|
+
break;
|
|
18899
|
+
}
|
|
18900
|
+
}
|
|
18865
18901
|
void args.resources.markDirty(URI_UPDATES);
|
|
18866
18902
|
return;
|
|
18867
18903
|
}
|
|
@@ -19549,8 +19585,25 @@ var URI_CURRENT7 = "memory://current";
|
|
|
19549
19585
|
var URI_OWNERS = "memory://owners";
|
|
19550
19586
|
var URI_GROWING = "memory://owners/growing";
|
|
19551
19587
|
var URI_LEAKS = "memory://leaks/suspected";
|
|
19588
|
+
var MAX_LEAKS = 200;
|
|
19589
|
+
var MAX_FLOWIDS_PER_OWNER = 50;
|
|
19552
19590
|
function installMemoryReducer(args) {
|
|
19553
19591
|
const state2 = { owners: /* @__PURE__ */ new Map(), leaks: [] };
|
|
19592
|
+
const pushLeak = (leak) => {
|
|
19593
|
+
state2.leaks.push(leak);
|
|
19594
|
+
if (state2.leaks.length > MAX_LEAKS)
|
|
19595
|
+
state2.leaks.splice(0, state2.leaks.length - MAX_LEAKS);
|
|
19596
|
+
};
|
|
19597
|
+
const noteFlow = (o, flowId) => {
|
|
19598
|
+
o.flowIds.add(flowId);
|
|
19599
|
+
if (o.flowIds.size > MAX_FLOWIDS_PER_OWNER) {
|
|
19600
|
+
for (const id of o.flowIds) {
|
|
19601
|
+
o.flowIds.delete(id);
|
|
19602
|
+
if (o.flowIds.size <= MAX_FLOWIDS_PER_OWNER)
|
|
19603
|
+
break;
|
|
19604
|
+
}
|
|
19605
|
+
}
|
|
19606
|
+
};
|
|
19554
19607
|
const ensure = (ownerId) => {
|
|
19555
19608
|
let o = state2.owners.get(ownerId);
|
|
19556
19609
|
if (!o) {
|
|
@@ -19581,7 +19634,7 @@ function installMemoryReducer(args) {
|
|
|
19581
19634
|
o.estimatedBytes = raw.mutation.afterCount * 64;
|
|
19582
19635
|
o.mutations += 1;
|
|
19583
19636
|
if (event.context.flowId)
|
|
19584
|
-
o
|
|
19637
|
+
noteFlow(o, event.context.flowId);
|
|
19585
19638
|
void args.resources.markDirty(URI_OWNERS);
|
|
19586
19639
|
void args.resources.markDirty(URI_GROWING);
|
|
19587
19640
|
return;
|
|
@@ -19589,7 +19642,7 @@ function installMemoryReducer(args) {
|
|
|
19589
19642
|
case "memory-retention": {
|
|
19590
19643
|
if (!raw.retention)
|
|
19591
19644
|
return;
|
|
19592
|
-
|
|
19645
|
+
pushLeak({
|
|
19593
19646
|
kind: "retention",
|
|
19594
19647
|
ownerId: raw.retention.ownerId,
|
|
19595
19648
|
flowId: raw.retention.flowId,
|
|
@@ -19602,7 +19655,7 @@ function installMemoryReducer(args) {
|
|
|
19602
19655
|
case "singleton-stale-generation": {
|
|
19603
19656
|
if (!raw.staleGeneration)
|
|
19604
19657
|
return;
|
|
19605
|
-
|
|
19658
|
+
pushLeak({
|
|
19606
19659
|
kind: "stale-generation",
|
|
19607
19660
|
logicalId: raw.staleGeneration.logicalId,
|
|
19608
19661
|
detail: { ...raw.staleGeneration },
|
package/lib/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gEAAgE;IAChE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AA6FD,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA4ChE;AA0nBD,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEnE;;;;;;;;iFAQiF;AACjF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,mBAAmB,CAItB"}
|
package/lib/cli.js
CHANGED
|
@@ -2,9 +2,10 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { request as httpRequest } from 'node:http';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
|
-
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { ensureLensConfig } from './workspace-scope.js';
|
|
8
|
+
import { sweepLensRuntimeFiles } from './sweep.js';
|
|
8
9
|
const HELP = `lensmcp — CLI for LensMCP (FrontMCP-based observability for coding agents)
|
|
9
10
|
|
|
10
11
|
Usage:
|
|
@@ -12,10 +13,16 @@ Usage:
|
|
|
12
13
|
|
|
13
14
|
Commands:
|
|
14
15
|
setup [--cwd <dir>] [--skip-format] [--register-host-config <mode>]
|
|
15
|
-
First-time bootstrap —
|
|
16
|
+
First-time bootstrap — \`install\` → \`trust\` → \`pods\` in
|
|
16
17
|
one idempotent pass: plugin + .mcp.json + nx config +
|
|
17
18
|
vite/nest wiring, then the dev CA (System keychain),
|
|
18
|
-
/etc/hosts (both families) + DNS flush
|
|
19
|
+
/etc/hosts (both families) + DNS flush, then a recycle of
|
|
20
|
+
THIS workspace's devserver pods (they would otherwise keep
|
|
21
|
+
serving the wiring setup just replaced) plus a gateway
|
|
22
|
+
reconcile — nx graph refresh, adopt an orphaned :443,
|
|
23
|
+
register this workspace into a running daemon. A running
|
|
24
|
+
gateway is NEVER restarted (the :443 door is shared) and
|
|
25
|
+
other workspaces' pods are never touched. sudo self-prompts
|
|
19
26
|
only for the steps that need it — run in a real terminal.
|
|
20
27
|
Then \`lensmcp gateway start\`.
|
|
21
28
|
mcp [--cwd <dir>] [--transport stdio|http] [--port <n>]
|
|
@@ -68,14 +75,16 @@ Commands:
|
|
|
68
75
|
promote, abort, add-rule, remove-rule, status. Validated +
|
|
69
76
|
atomic write; the prod gateway picks it up live. Run
|
|
70
77
|
\`lensmcp rollout\` (no args) for the op list.
|
|
71
|
-
logs [service] [--tail <n>] [--no-follow] [--plain] [--cwd <dir>]
|
|
78
|
+
logs [service] [--tail <n>] [--no-follow] [--plain] [--color] [--cwd <dir>]
|
|
72
79
|
Attach this terminal to a running service's live log
|
|
73
80
|
stream (docker-logs style — for humans and agents alike).
|
|
74
81
|
No <service> lists every attachable service across
|
|
75
82
|
workspaces. --tail <n> replays that many history lines
|
|
76
83
|
first (default 100); --no-follow prints them and exits
|
|
77
84
|
(the agent-friendly snapshot); --plain strips ANSI color
|
|
78
|
-
(automatic when output is piped)
|
|
85
|
+
(automatic when output is piped); --color FORCES color
|
|
86
|
+
through a pipe — for consumers that RENDER ANSI, like
|
|
87
|
+
the IDE plugin's Logs console. Needs no ports — it
|
|
79
88
|
finds the devserver's control socket under $TMPDIR.
|
|
80
89
|
debug <service> [--port <base>] [--cwd <dir>]
|
|
81
90
|
Open every pod's Node inspector of a RUNNING service —
|
|
@@ -239,6 +248,15 @@ function daemonRequest(method, urlPath, body) {
|
|
|
239
248
|
catch { /* non-JSON body */ }
|
|
240
249
|
return Number.isFinite(status) ? { status, json } : null;
|
|
241
250
|
}
|
|
251
|
+
/** The workspace key that OWNS the running daemon (undefined when no daemon answers) — used to name the
|
|
252
|
+
* workspace a guest must run privileged/workspace-scoped work (`trust`) in. */
|
|
253
|
+
function daemonWsKey() {
|
|
254
|
+
const s = daemonRequest('GET', '/status');
|
|
255
|
+
if (s?.status !== 200 || !s.json || typeof s.json !== 'object')
|
|
256
|
+
return undefined;
|
|
257
|
+
const d = s.json.daemon;
|
|
258
|
+
return typeof d?.wsKey === 'string' && d.wsKey.length > 0 ? d.wsKey : undefined;
|
|
259
|
+
}
|
|
242
260
|
/** The nx projects map (name → workspace-relative root) for cluster discovery — built by scanning
|
|
243
261
|
* project.json files, the same walk `findGatewayTarget` uses. Sent to the daemon so it can `discoverRoutes`
|
|
244
262
|
* this workspace when it registers. */
|
|
@@ -336,8 +354,17 @@ function runGateway(ctx, args, out, err) {
|
|
|
336
354
|
}
|
|
337
355
|
const target = findGatewayTarget(cwd, stringFlag(opts.flags['--project']), stringFlag(opts.flags['--target']));
|
|
338
356
|
if (!target) {
|
|
339
|
-
|
|
340
|
-
|
|
357
|
+
// This workspace hosts no gateway of its own. That is a NORMAL state for a guest workspace — it just
|
|
358
|
+
// means there is nothing on :443 to join right now. Say what to actually do, in order of likelihood,
|
|
359
|
+
// instead of only naming the two flags (the papercut the IDE plugin surfaced verbatim).
|
|
360
|
+
err(`This workspace ('${cfg.key}') has no gateway target — no project declares the`);
|
|
361
|
+
err('`@lensmcp/cluster:gateway` executor, so there is nothing here to launch on :443.');
|
|
362
|
+
err('');
|
|
363
|
+
err(' • Joining a shared gateway? Start it in the workspace that OWNS it, then run');
|
|
364
|
+
err(' `lensmcp gateway start` here — this workspace registers into it (no local target needed).');
|
|
365
|
+
err(' • Should this workspace host its own gateway? Scaffold it: `lensmcp install`');
|
|
366
|
+
err(' (or `lensmcp setup` for the full install + trust bootstrap).');
|
|
367
|
+
err(' • Already have a differently-named target? `lensmcp gateway start --project <p> --target <t>`.');
|
|
341
368
|
return { exitCode: 1 };
|
|
342
369
|
}
|
|
343
370
|
const nxBin = findNxBinary(cwd);
|
|
@@ -345,6 +372,7 @@ function runGateway(ctx, args, out, err) {
|
|
|
345
372
|
err('Could not find the `nx` binary in the workspace. Install Nx first: `yarn add -D nx`.');
|
|
346
373
|
return { exitCode: 1 };
|
|
347
374
|
}
|
|
375
|
+
sweepLensRuntimeFiles(cwd, logFile);
|
|
348
376
|
const fd = openSync(logFile, 'a');
|
|
349
377
|
const child = spawn(nxBin, ['run', `${target.project}:${target.target}`], {
|
|
350
378
|
cwd,
|
|
@@ -586,14 +614,34 @@ function findGatewayTarget(cwd, project, target) {
|
|
|
586
614
|
// Sensible fallback used by most workspaces (tetros: tools/gateway → `gateway:serve`).
|
|
587
615
|
return project || target ? { project: project ?? 'gateway', target: target ?? 'serve' } : undefined;
|
|
588
616
|
}
|
|
589
|
-
/**
|
|
590
|
-
*
|
|
591
|
-
*
|
|
617
|
+
/**
|
|
618
|
+
* Find a project + target whose executor is `@lensmcp/cluster:trust`. Falls back to `gateway:trust` — the
|
|
619
|
+
* conventional home the init generator scaffolds — but ONLY when a project named `gateway` actually exists
|
|
620
|
+
* (an explicit `--project` is always taken at face value). The old unconditional fallback made `trust` in a
|
|
621
|
+
* workspace that never ran `lensmcp install` shell out to `nx run gateway:trust` and die on Nx's opaque
|
|
622
|
+
* "Cannot find project 'gateway'"; returning undefined lets the caller say what to do instead.
|
|
623
|
+
*/
|
|
592
624
|
function findTrustTarget(cwd, project, target) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
625
|
+
const found = scanExecutorTarget(cwd, '@lensmcp/cluster:trust', project, target);
|
|
626
|
+
if (found)
|
|
627
|
+
return found;
|
|
628
|
+
if (project)
|
|
629
|
+
return { project, target: target ?? 'trust' };
|
|
630
|
+
return projectExists(cwd, 'gateway') ? { project: 'gateway', target: target ?? 'trust' } : undefined;
|
|
631
|
+
}
|
|
632
|
+
/** Does a project of this name exist in the workspace? (project.json-declared, like every other scan here.) */
|
|
633
|
+
function projectExists(cwd, name) {
|
|
634
|
+
for (const file of walkProjectFiles(cwd, (n) => n === 'project.json')) {
|
|
635
|
+
try {
|
|
636
|
+
const parsed = JSON.parse(safeRead(file));
|
|
637
|
+
if ((parsed.name ?? basename(dirname(file))) === name)
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
/* unreadable project.json — not a match */
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return false;
|
|
597
645
|
}
|
|
598
646
|
function readPid(pidFile) {
|
|
599
647
|
try {
|
|
@@ -867,6 +915,26 @@ function runTrust(ctx, args, out, err) {
|
|
|
867
915
|
// that unused CA file (the papercut). So run HOSTS-ONLY (/etc/hosts + DNS) and report the REAL served-cert
|
|
868
916
|
// trust instead.
|
|
869
917
|
const guestOfDaemon = daemonRequest('GET', '/list')?.status === 200 && gatewayOnPort443(cwd) === undefined;
|
|
918
|
+
// No trust target anywhere in this workspace (never ran `lensmcp install`). Trust is workspace-scoped work
|
|
919
|
+
// — minting the CA and writing /etc/hosts for THIS workspace's cluster hosts — so there is nothing to run
|
|
920
|
+
// here. Say which workspace owns the CA instead of failing inside Nx with "Cannot find project 'gateway'".
|
|
921
|
+
if (!target) {
|
|
922
|
+
err(`This workspace has no trust target — no project declares the \`@lensmcp/cluster:trust\` executor.`);
|
|
923
|
+
err('');
|
|
924
|
+
if (guestOfDaemon) {
|
|
925
|
+
const owner = daemonWsKey();
|
|
926
|
+
err(` A shared gateway daemon${owner ? ` (owned by '${owner}')` : ''} already serves this workspace, and`);
|
|
927
|
+
err(' TLS rides ITS certificate authority. Run `lensmcp trust` in THAT workspace — it owns the CA —');
|
|
928
|
+
err(' and this workspace is trusted too (restart the browser once if it cached a cert error).');
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
err(' • Should this workspace own its gateway (and its CA)? Scaffold it first: `lensmcp install`,');
|
|
932
|
+
err(' then re-run `lensmcp trust`.');
|
|
933
|
+
err(' • Joining a shared gateway? Run `lensmcp trust` in the workspace that owns the daemon.');
|
|
934
|
+
}
|
|
935
|
+
err(' • Already have a differently-named target? `lensmcp trust --project <p> --target <t>`.');
|
|
936
|
+
return { exitCode: 1 };
|
|
937
|
+
}
|
|
870
938
|
if (!isInteractive()) {
|
|
871
939
|
out(' note: trust runs `sudo` for the CA + /etc/hosts — run it in a real terminal if a step is needed.');
|
|
872
940
|
}
|
|
@@ -920,8 +988,10 @@ function writeSetupState(cwd) {
|
|
|
920
988
|
}
|
|
921
989
|
/**
|
|
922
990
|
* `lensmcp setup` — first-time bootstrap. Runs `install` (plugin + .mcp.json +
|
|
923
|
-
* nx config + vite/nest wiring, no sudo) then `trust` (dev CA → keychain,
|
|
924
|
-
* /etc/hosts, DNS flush, sudo)
|
|
991
|
+
* nx config + vite/nest wiring, no sudo), then `trust` (dev CA → keychain,
|
|
992
|
+
* /etc/hosts, DNS flush, sudo), then `pods` — which recycles this workspace's
|
|
993
|
+
* devservers and reconciles the gateway, so nothing keeps serving the wiring
|
|
994
|
+
* that setup just replaced. Every step is idempotent, so re-running is safe.
|
|
925
995
|
* Flags are forwarded to whichever sub-command understands them (`--skip-format`,
|
|
926
996
|
* `--register-host-config` → install; `--project`/`--target` → trust).
|
|
927
997
|
*/
|
|
@@ -929,9 +999,9 @@ function runSetup(ctx, args, out, err) {
|
|
|
929
999
|
const opts = parseFlags(args, { string: ['--cwd'] });
|
|
930
1000
|
const cwd = resolve(stringFlag(opts.flags['--cwd']) ?? ctx.cwd);
|
|
931
1001
|
const subCtx = { ...ctx, cwd };
|
|
932
|
-
out('lensmcp setup — first-time bootstrap (install → trust)');
|
|
1002
|
+
out('lensmcp setup — first-time bootstrap (install → trust → pods)');
|
|
933
1003
|
out('');
|
|
934
|
-
out('[1/
|
|
1004
|
+
out('[1/3] install — plugin, .mcp.json, nx config, vite/nest wiring');
|
|
935
1005
|
const installRes = runInstall(subCtx, args, out, err);
|
|
936
1006
|
if (installRes.exitCode !== 0) {
|
|
937
1007
|
err('');
|
|
@@ -939,17 +1009,31 @@ function runSetup(ctx, args, out, err) {
|
|
|
939
1009
|
return installRes;
|
|
940
1010
|
}
|
|
941
1011
|
out('');
|
|
942
|
-
out('[2/
|
|
1012
|
+
out('[2/3] trust — dev CA → System keychain, /etc/hosts, DNS flush (sudo)');
|
|
943
1013
|
const trustRes = runTrust(subCtx, args, out, err);
|
|
944
1014
|
if (trustRes.exitCode !== 0) {
|
|
945
1015
|
err('');
|
|
946
1016
|
err('setup: the trust step failed — run `lensmcp setup` directly in a terminal so sudo can prompt.');
|
|
947
1017
|
return trustRes;
|
|
948
1018
|
}
|
|
949
|
-
//
|
|
1019
|
+
// Install + trust rewrote this workspace's wiring, but a devserver that has been up since before it
|
|
1020
|
+
// changed goes on serving the OLD bundle from its pooled pods — the "setup said ✓ but the app is
|
|
1021
|
+
// unchanged" trap. Recycle them, then put the gateway back in line with reality WITHOUT restarting it
|
|
1022
|
+
// (the :443 door is shared: bouncing it would take every other workspace down with this one).
|
|
1023
|
+
out('');
|
|
1024
|
+
out("[3/3] pods — recycle this workspace's devservers, reconcile the gateway");
|
|
1025
|
+
const key = ensureLensConfig(cwd).key;
|
|
1026
|
+
const reaped = reapWorkspacePods(cwd, key, out);
|
|
1027
|
+
reconcileGatewayAfterSetup(subCtx, cwd, key, out);
|
|
1028
|
+
// All steps succeeded → mark the workspace set up for this version (opens the MCP gate).
|
|
950
1029
|
writeSetupState(cwd);
|
|
951
1030
|
out('');
|
|
952
1031
|
out('✓ setup complete.');
|
|
1032
|
+
if (reaped.killed > 0 || reaped.stale > 0) {
|
|
1033
|
+
out(` recycled ${reaped.killed} pod${reaped.killed === 1 ? '' : 's'}` +
|
|
1034
|
+
(reaped.stale > 0 ? ` (+${reaped.stale} stale socket${reaped.stale === 1 ? '' : 's'} cleared)` : '') +
|
|
1035
|
+
' — they come back on the new wiring.');
|
|
1036
|
+
}
|
|
953
1037
|
out(' next: lensmcp gateway start (the :443 front door — needs sudo)');
|
|
954
1038
|
out(' verify: lensmcp doctor');
|
|
955
1039
|
return { exitCode: 0 };
|
|
@@ -1309,6 +1393,179 @@ function discoverDevserverSocks() {
|
|
|
1309
1393
|
return found.sort((a, b) => `${a.wsKey}/${a.service}`.localeCompare(`${b.wsKey}/${b.service}`));
|
|
1310
1394
|
}
|
|
1311
1395
|
const fullServiceName = (d) => (d.wsKey ? `${d.wsKey}/${d.service}` : d.service);
|
|
1396
|
+
/* ───────── pod recycling (the `setup` reap) ─────────
|
|
1397
|
+
* A devserver is a PARENT process holding `parent.sock` plus N forked child pods. Killing the parent is
|
|
1398
|
+
* enough for the pods — they exit on the IPC `disconnect` (main.devserver.ts's ORPHAN GUARD) and unlink
|
|
1399
|
+
* their own sockets. But the gateway spawns each service `detached: true` with `NX_DAEMON=false`, so the
|
|
1400
|
+
* whole service — the `nx run` shim, the devserver, the pods — sits in ONE process group of its own; group-
|
|
1401
|
+
* killing that reaps the shim too, which a lone parent kill would leave behind (the documented "nx wrapper
|
|
1402
|
+
* outlives its child" leak). Hence: kill the group when we can prove it's the service's own, else the pid. */
|
|
1403
|
+
/** The pid holding a unix socket (best-effort via lsof; undefined = the socket is stale litter). */
|
|
1404
|
+
function pidOnSocket(sock) {
|
|
1405
|
+
try {
|
|
1406
|
+
const r = spawnSync('lsof', ['-t', sock], { encoding: 'utf8' });
|
|
1407
|
+
const pid = Number((r.stdout ?? '').split('\n')[0]?.trim());
|
|
1408
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
1409
|
+
}
|
|
1410
|
+
catch {
|
|
1411
|
+
return undefined;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/** A process's group id (undefined when it's gone or `ps` is unavailable). */
|
|
1415
|
+
function pgidOf(pid) {
|
|
1416
|
+
try {
|
|
1417
|
+
const r = spawnSync('ps', ['-o', 'pgid=', '-p', String(pid)], { encoding: 'utf8' });
|
|
1418
|
+
const pgid = Number((r.stdout ?? '').trim());
|
|
1419
|
+
return Number.isInteger(pgid) && pgid > 1 ? pgid : undefined;
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
/** A process's working directory — how an UN-NAMESPACED devserver (a plain `nx serve`, no LENSMCP_WS_KEY)
|
|
1426
|
+
* gets attributed to a workspace. Undefined when it can't be read, and an unattributable pod is left
|
|
1427
|
+
* alone: it belongs to somebody else until proven otherwise. */
|
|
1428
|
+
function cwdOfPid(pid) {
|
|
1429
|
+
try {
|
|
1430
|
+
const r = spawnSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8' });
|
|
1431
|
+
const line = (r.stdout ?? '').split('\n').find((l) => l.startsWith('n'));
|
|
1432
|
+
return line ? line.slice(1).trim() || undefined : undefined;
|
|
1433
|
+
}
|
|
1434
|
+
catch {
|
|
1435
|
+
return undefined;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/** Is `child` the same path as, or inside, `parent`? */
|
|
1439
|
+
function isInside(parent, child) {
|
|
1440
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
1441
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
1442
|
+
}
|
|
1443
|
+
/** Every devserver under $TMPDIR that belongs to the workspace rooted at `cwd`. A namespaced sock dir
|
|
1444
|
+
* (`$TMPDIR/<wsKey>/…`) is attributed by its key; a legacy un-namespaced one by its owner's cwd. */
|
|
1445
|
+
function workspaceDevservers(cwd, key) {
|
|
1446
|
+
return discoverDevserverSocks().filter((d) => {
|
|
1447
|
+
if (d.wsKey)
|
|
1448
|
+
return d.wsKey === key;
|
|
1449
|
+
const pid = pidOnSocket(d.sock);
|
|
1450
|
+
const owner = pid !== undefined ? cwdOfPid(pid) : undefined;
|
|
1451
|
+
return owner !== undefined && isInside(cwd, owner);
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Stop every devserver of THIS workspace and clear its socket dir, so the next boot comes up on the
|
|
1456
|
+
* freshly installed wiring instead of a pod that has been running since before it changed.
|
|
1457
|
+
*
|
|
1458
|
+
* Safe next to a live gateway: it marks a vanished service `down` and either self-heals it or scales it
|
|
1459
|
+
* from zero on the next request, so the front door keeps serving while the pods come back new. NEVER
|
|
1460
|
+
* group-kills a group it can't prove is the service's own — a mis-derived group would take down the
|
|
1461
|
+
* gateway (or this CLI) with it.
|
|
1462
|
+
*/
|
|
1463
|
+
function reapWorkspacePods(cwd, key, out) {
|
|
1464
|
+
const mine = workspaceDevservers(cwd, key);
|
|
1465
|
+
if (mine.length === 0) {
|
|
1466
|
+
out(' no devserver pods running for this workspace — nothing to recycle.');
|
|
1467
|
+
return { killed: 0, stale: 0 };
|
|
1468
|
+
}
|
|
1469
|
+
// Groups we must never signal: our own, and whatever holds :443 (the gateway must survive this).
|
|
1470
|
+
const forbidden = new Set();
|
|
1471
|
+
for (const p of [process.pid, ...pidsOnPort443()]) {
|
|
1472
|
+
const g = pgidOf(p);
|
|
1473
|
+
if (g !== undefined)
|
|
1474
|
+
forbidden.add(g);
|
|
1475
|
+
}
|
|
1476
|
+
let killed = 0;
|
|
1477
|
+
let stale = 0;
|
|
1478
|
+
for (const d of mine) {
|
|
1479
|
+
const name = fullServiceName(d);
|
|
1480
|
+
const pid = pidOnSocket(d.sock);
|
|
1481
|
+
if (pid === undefined) {
|
|
1482
|
+
rmFile(d.sock);
|
|
1483
|
+
out(` ✓ ${name} — stale socket removed (no process held it)`);
|
|
1484
|
+
stale++;
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
const group = pgidOf(pid);
|
|
1488
|
+
const groupKillable = group !== undefined && !forbidden.has(group);
|
|
1489
|
+
const signal = (sig) => {
|
|
1490
|
+
try {
|
|
1491
|
+
if (groupKillable)
|
|
1492
|
+
process.kill(-group, sig);
|
|
1493
|
+
else
|
|
1494
|
+
process.kill(pid, sig); // pods follow the parent out via the IPC disconnect guard
|
|
1495
|
+
}
|
|
1496
|
+
catch {
|
|
1497
|
+
/* already gone */
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
signal('SIGTERM');
|
|
1501
|
+
// Give the tree a moment to unwind gracefully (the devserver closes its pods' IPC, they unlink their
|
|
1502
|
+
// own sockets), then escalate — a wedged child must not survive the recycle.
|
|
1503
|
+
if (!waitForPidGone(pid, 5000)) {
|
|
1504
|
+
signal('SIGKILL');
|
|
1505
|
+
waitForPidGone(pid, 2000);
|
|
1506
|
+
}
|
|
1507
|
+
rmFile(d.sock);
|
|
1508
|
+
out(` ✓ ${name} — stopped (pid ${pid}${groupKillable ? `, group ${group}` : ', parent only'})`);
|
|
1509
|
+
killed++;
|
|
1510
|
+
}
|
|
1511
|
+
return { killed, stale };
|
|
1512
|
+
}
|
|
1513
|
+
/** Poll until `pid` is gone or the timeout elapses. Synchronous — a setup step, not a hot path. */
|
|
1514
|
+
function waitForPidGone(pid, timeoutMs) {
|
|
1515
|
+
const deadline = Date.now() + timeoutMs;
|
|
1516
|
+
for (;;) {
|
|
1517
|
+
if (!isAlive(pid))
|
|
1518
|
+
return true;
|
|
1519
|
+
if (Date.now() >= deadline)
|
|
1520
|
+
return false;
|
|
1521
|
+
spawnSync('sleep', ['0.15']);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Bring the gateway back in line with reality WITHOUT restarting a running one — a `setup` must never take
|
|
1526
|
+
* the shared `:443` front door (and every other workspace on it) down. It:
|
|
1527
|
+
* • adopts an orphaned gateway the pid file lost track of (the desync self-heal);
|
|
1528
|
+
* • busts the nx project graph, so the projects `install` just wired resolve instead of 404ing out of a
|
|
1529
|
+
* daemon whose graph predates them;
|
|
1530
|
+
* • registers this workspace into a daemon someone else owns, when it isn't in there yet.
|
|
1531
|
+
* An already-registered workspace is left alone: re-registering makes the daemon tear down and respawn its
|
|
1532
|
+
* services and dashboard for nothing.
|
|
1533
|
+
*/
|
|
1534
|
+
function reconcileGatewayAfterSetup(ctx, cwd, key, out) {
|
|
1535
|
+
const pidFile = join(cwd, '.lensmcp', 'gateway.pid');
|
|
1536
|
+
const registeredMarker = join(cwd, '.lensmcp', 'registered.json');
|
|
1537
|
+
const { pid, healed } = reconcileGateway(pidFile, cwd);
|
|
1538
|
+
if (healed)
|
|
1539
|
+
out(` healed: adopted a running gateway (pid ${pid}) the pid file had lost.`);
|
|
1540
|
+
refreshNxGraph(cwd, ctx.env, (l) => out(` ${l}`));
|
|
1541
|
+
const owned = gatewayOnPort443(cwd) !== undefined;
|
|
1542
|
+
if (owned) {
|
|
1543
|
+
out(` gateway: this workspace owns the :443 daemon (pid ${pid ?? '?'}) — left running, pods respawn fresh.`);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
const list = daemonRequest('GET', '/list');
|
|
1547
|
+
if (list?.status !== 200) {
|
|
1548
|
+
out(' gateway: not running. Start it when you are ready: lensmcp gateway start');
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const registered = list.json && typeof list.json === 'object'
|
|
1552
|
+
? (list.json.workspaces ?? []).some((w) => w.key === key)
|
|
1553
|
+
: false;
|
|
1554
|
+
if (registered) {
|
|
1555
|
+
mkdirSync(dirname(registeredMarker), { recursive: true });
|
|
1556
|
+
writeFileSync(registeredMarker, JSON.stringify({ wsKey: key }));
|
|
1557
|
+
out(` gateway: '${key}' is already registered into the shared daemon — left serving.`);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const res = daemonRequest('POST', '/register', { wsKey: key, root: cwd, projects: buildProjectsMap(cwd) });
|
|
1561
|
+
if (res && res.status === 200) {
|
|
1562
|
+
mkdirSync(dirname(registeredMarker), { recursive: true });
|
|
1563
|
+
writeFileSync(registeredMarker, JSON.stringify({ wsKey: key }));
|
|
1564
|
+
out(` gateway: registered '${key}' into the shared daemon — it now hosts this workspace.`);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
out(' gateway: a shared daemon is up but registering this workspace failed — run `lensmcp gateway start`.');
|
|
1568
|
+
}
|
|
1312
1569
|
/** Resolve a service argument (`name` or `wsKey/name`) to ONE control socket. A bare
|
|
1313
1570
|
* name running in several workspaces is disambiguated by the current workspace's key.
|
|
1314
1571
|
* Prints the helpful error itself and returns null when nothing (or too much) matches. */
|
|
@@ -1340,7 +1597,7 @@ function resolveServiceSock(service, all, cwd, err) {
|
|
|
1340
1597
|
async function runLogs(ctx, args, out, err) {
|
|
1341
1598
|
const opts = parseFlags(args, {
|
|
1342
1599
|
string: ['--tail', '--cwd'],
|
|
1343
|
-
boolean: ['--no-follow', '--plain'],
|
|
1600
|
+
boolean: ['--no-follow', '--plain', '--color'],
|
|
1344
1601
|
});
|
|
1345
1602
|
const service = opts.positional[0];
|
|
1346
1603
|
const all = discoverDevserverSocks();
|
|
@@ -1362,7 +1619,11 @@ async function runLogs(ctx, args, out, err) {
|
|
|
1362
1619
|
const follow = opts.flags['--no-follow'] !== true;
|
|
1363
1620
|
const tailRaw = Number(stringFlag(opts.flags['--tail']) ?? NaN);
|
|
1364
1621
|
const tail = Number.isInteger(tailRaw) && tailRaw >= 0 ? tailRaw : 100; // 0 = live-only, no history
|
|
1365
|
-
|
|
1622
|
+
// ANSI is stripped when the output is piped — EXCEPT for a consumer that renders it and says so with
|
|
1623
|
+
// `--color` (the IDE plugin's Logs console runs through a pipe, and plain-stripped output is exactly the
|
|
1624
|
+
// unreadable wall of text the colored stream avoids). Explicit `--plain` still wins.
|
|
1625
|
+
const plain = opts.flags['--plain'] === true ||
|
|
1626
|
+
(opts.flags['--color'] !== true && !process.stdout.isTTY);
|
|
1366
1627
|
const reqPath = `/webpack/logs?tail=${tail}&follow=${follow ? 1 : 0}${plain ? '&plain=1' : ''}`;
|
|
1367
1628
|
return new Promise((done) => {
|
|
1368
1629
|
const req = httpRequest({ socketPath: target.sock, path: reqPath, method: 'GET' }, (res) => {
|
package/lib/sweep.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention sweep at gateway start — the one moment we KNOW no gateway is writing these files.
|
|
3
|
+
* The bus rotates round-robin (`events.jsonl.1` … `.N`, 50 MB per generation, oldest dropped —
|
|
4
|
+
* emit.ts's `LENSMCP_EVENT_MAX_BYTES`/`LENSMCP_EVENT_MAX_FILES` contract); this prunes what that
|
|
5
|
+
* scheme can't reach: legacy `.old` files older schemes left behind (a tetros workspace carried an
|
|
6
|
+
* 8.6 GB `events.jsonl.old` from July), generations beyond the horizon, oversized generations from
|
|
7
|
+
* the pre-cap era, and a grown `gateway.log` (rotated to `.1` so the append-only log doesn't creep
|
|
8
|
+
* to hundreds of MB across restarts). Lives outside cli.ts so it stays CJS-jestable (cli.ts uses
|
|
9
|
+
* import.meta).
|
|
10
|
+
*/
|
|
11
|
+
export declare function sweepLensRuntimeFiles(cwd: string, logFile: string): void;
|
|
12
|
+
//# sourceMappingURL=sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sweep.d.ts","sourceRoot":"","sources":["../../src/lib/sweep.ts"],"names":[],"mappings":"AAWA;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAgCxE"}
|
package/lib/sweep.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const removeQuietly = (path) => {
|
|
4
|
+
try {
|
|
5
|
+
rmSync(path, { force: true });
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
/* best-effort — a vanished file or racing sweep must never block a start */
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Retention sweep at gateway start — the one moment we KNOW no gateway is writing these files.
|
|
13
|
+
* The bus rotates round-robin (`events.jsonl.1` … `.N`, 50 MB per generation, oldest dropped —
|
|
14
|
+
* emit.ts's `LENSMCP_EVENT_MAX_BYTES`/`LENSMCP_EVENT_MAX_FILES` contract); this prunes what that
|
|
15
|
+
* scheme can't reach: legacy `.old` files older schemes left behind (a tetros workspace carried an
|
|
16
|
+
* 8.6 GB `events.jsonl.old` from July), generations beyond the horizon, oversized generations from
|
|
17
|
+
* the pre-cap era, and a grown `gateway.log` (rotated to `.1` so the append-only log doesn't creep
|
|
18
|
+
* to hundreds of MB across restarts). Lives outside cli.ts so it stays CJS-jestable (cli.ts uses
|
|
19
|
+
* import.meta).
|
|
20
|
+
*/
|
|
21
|
+
export function sweepLensRuntimeFiles(cwd, logFile) {
|
|
22
|
+
const dir = join(cwd, '.lensmcp');
|
|
23
|
+
const eventFile = join(dir, 'events.jsonl');
|
|
24
|
+
removeQuietly(`${eventFile}.old`);
|
|
25
|
+
removeQuietly(`${logFile}.old`);
|
|
26
|
+
const maxFiles = (() => {
|
|
27
|
+
const n = Number(process.env['LENSMCP_EVENT_MAX_FILES']);
|
|
28
|
+
return Number.isFinite(n) && n >= 1 && n <= 20 ? Math.floor(n) : 5;
|
|
29
|
+
})();
|
|
30
|
+
const MAX_GENERATION_BYTES = 64 * 1024 * 1024; // legit generations are ≤ ~50 MB; bigger = pre-cap era litter
|
|
31
|
+
try {
|
|
32
|
+
for (const name of readdirSync(dir)) {
|
|
33
|
+
const m = /^events\.jsonl\.(\d+)$/.exec(name);
|
|
34
|
+
if (!m)
|
|
35
|
+
continue;
|
|
36
|
+
const gen = Number(m[1]);
|
|
37
|
+
const file = join(dir, name);
|
|
38
|
+
if (gen < 1 || gen > maxFiles)
|
|
39
|
+
removeQuietly(file);
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
if (statSync(file).size > MAX_GENERATION_BYTES)
|
|
43
|
+
removeQuietly(file);
|
|
44
|
+
}
|
|
45
|
+
catch { /* raced away */ }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* no .lensmcp dir yet */
|
|
51
|
+
}
|
|
52
|
+
const MAX_GATEWAY_LOG_BYTES = 16 * 1024 * 1024;
|
|
53
|
+
try {
|
|
54
|
+
if (statSync(logFile).size > MAX_GATEWAY_LOG_BYTES)
|
|
55
|
+
renameSync(logFile, `${logFile}.1`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* no log yet */
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "lensmcp",
|
|
3
3
|
"displayName": "LensMCP",
|
|
4
4
|
"description": "The observability lens for coding agents. One command brings up the dev cluster gateway (every project.json `cluster` decl → its host on :443), the per-project lens dashboard at https://lensmcp.local/<project>/, and the MCP server your agent connects to — scoped automatically to whatever project you opened Claude Code in.",
|
|
5
|
-
"version": "1.16.
|
|
5
|
+
"version": "1.16.31",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "David Antoon",
|
|
8
8
|
"email": "davidmantoon@gmail.com"
|