lensmcp 1.16.12 → 1.16.13
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/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +76 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
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":"AAOA,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;AAqED,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAwChE"}
|
package/lib/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { ensureLensConfig } from './workspace-scope.js';
|
|
6
7
|
const HELP = `lensmcp — CLI for LensMCP (FrontMCP-based observability for coding agents)
|
|
@@ -191,6 +192,47 @@ function runDashboard(ctx, args, out, err) {
|
|
|
191
192
|
* :443, and serves the per-workspace lens dashboard at https://lensmcp.local/<key>/.
|
|
192
193
|
* This wraps it so a single command (and the Claude Code plugin) can bring it up/down.
|
|
193
194
|
*/
|
|
195
|
+
/** The daemon control socket (mirrors `@lensmcp/cluster` control.ts — kept in sync by convention). */
|
|
196
|
+
function controlSocketPath() {
|
|
197
|
+
return join(process.env['LENSMCP_HOME'] || homedir(), '.lensmcp', 'control.sock');
|
|
198
|
+
}
|
|
199
|
+
/** One SYNC request to the daemon control socket via `curl --unix-socket` (the CLI paths are synchronous;
|
|
200
|
+
* Node has no sync HTTP). Returns the parsed { status, json } or null if the daemon isn't reachable. */
|
|
201
|
+
function daemonRequest(method, urlPath, body) {
|
|
202
|
+
const sock = controlSocketPath();
|
|
203
|
+
if (!existsSync(sock))
|
|
204
|
+
return null;
|
|
205
|
+
const curlArgs = ['-s', '-o', '-', '-w', '\n%{http_code}', '--unix-socket', sock, '-X', method, `http://localhost${urlPath}`];
|
|
206
|
+
if (body !== undefined)
|
|
207
|
+
curlArgs.push('-H', 'content-type: application/json', '-d', JSON.stringify(body));
|
|
208
|
+
const r = spawnSync('curl', curlArgs, { encoding: 'utf8', timeout: 5000 });
|
|
209
|
+
if (r.status !== 0 || typeof r.stdout !== 'string' || r.stdout.length === 0)
|
|
210
|
+
return null;
|
|
211
|
+
const lines = r.stdout.split('\n');
|
|
212
|
+
const status = Number(lines.pop());
|
|
213
|
+
let json = null;
|
|
214
|
+
try {
|
|
215
|
+
const t = lines.join('\n').trim();
|
|
216
|
+
json = t ? JSON.parse(t) : null;
|
|
217
|
+
}
|
|
218
|
+
catch { /* non-JSON body */ }
|
|
219
|
+
return Number.isFinite(status) ? { status, json } : null;
|
|
220
|
+
}
|
|
221
|
+
/** The nx projects map (name → workspace-relative root) for cluster discovery — built by scanning
|
|
222
|
+
* project.json files, the same walk `findGatewayTarget` uses. Sent to the daemon so it can `discoverRoutes`
|
|
223
|
+
* this workspace when it registers. */
|
|
224
|
+
function buildProjectsMap(cwd) {
|
|
225
|
+
const map = {};
|
|
226
|
+
for (const file of walkProjectFiles(cwd, (n) => n === 'project.json')) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
229
|
+
const dir = dirname(file);
|
|
230
|
+
map[parsed.name ?? basename(dir)] = { root: relative(cwd, dir) || '.' };
|
|
231
|
+
}
|
|
232
|
+
catch { /* skip an unreadable project.json */ }
|
|
233
|
+
}
|
|
234
|
+
return map;
|
|
235
|
+
}
|
|
194
236
|
function runGateway(ctx, args, out, err) {
|
|
195
237
|
const opts = parseFlags(args, { string: ['--cwd', '--project', '--target'] });
|
|
196
238
|
const sub = (opts.positional[0] ?? 'status').toLowerCase();
|
|
@@ -198,6 +240,9 @@ function runGateway(ctx, args, out, err) {
|
|
|
198
240
|
const cfg = ensureLensConfig(cwd);
|
|
199
241
|
const pidFile = join(cwd, '.lensmcp', 'gateway.pid');
|
|
200
242
|
const logFile = join(cwd, '.lensmcp', 'gateway.log');
|
|
243
|
+
// Set when THIS workspace registered into a shared daemon (P4) rather than owning :443 itself — so `stop`
|
|
244
|
+
// unregisters instead of trying to kill a gateway it never spawned.
|
|
245
|
+
const registeredMarker = join(cwd, '.lensmcp', 'registered.json');
|
|
201
246
|
const dashUrl = `https://lensmcp.local${cfg.dashboardBasePath}/`;
|
|
202
247
|
const start = () => {
|
|
203
248
|
// Reconcile with reality FIRST: an alive pid-file wins; else adopt an orphaned gateway on :443 (the
|
|
@@ -210,10 +255,25 @@ function runGateway(ctx, args, out, err) {
|
|
|
210
255
|
out(` dashboard → ${dashUrl}`);
|
|
211
256
|
return { exitCode: 0 };
|
|
212
257
|
}
|
|
213
|
-
// :443 is
|
|
214
|
-
//
|
|
258
|
+
// :443 is held by another process. If it's a lens DAEMON (its control socket answers), REGISTER this
|
|
259
|
+
// workspace INTO it — the daemon then hosts this workspace's services + dashboard under the one :443
|
|
260
|
+
// (planning/multi-workspace-gateway.md P4). Otherwise a FOREIGN holder: surface it (spawning would just
|
|
261
|
+
// EADDRINUSE-crash + orphan).
|
|
215
262
|
const foreign = pidsOnPort443()[0];
|
|
216
263
|
if (foreign !== undefined) {
|
|
264
|
+
if (daemonRequest('GET', '/list') !== null) {
|
|
265
|
+
const res = daemonRequest('POST', '/register', { wsKey: cfg.key, root: cwd, projects: buildProjectsMap(cwd) });
|
|
266
|
+
if (res && res.status === 200) {
|
|
267
|
+
mkdirSync(dirname(registeredMarker), { recursive: true });
|
|
268
|
+
writeFileSync(registeredMarker, JSON.stringify({ wsKey: cfg.key }));
|
|
269
|
+
out(`registered '${cfg.key}' into the shared gateway daemon (pid ${foreign}).`);
|
|
270
|
+
out(` dashboard → ${dashUrl}`);
|
|
271
|
+
out(" the daemon hosts this workspace's services + dashboard; `lensmcp gateway stop` unregisters.");
|
|
272
|
+
return { exitCode: 0 };
|
|
273
|
+
}
|
|
274
|
+
err(`failed to register into the gateway daemon: ${res ? JSON.stringify(res.json) : 'no response from the control socket'}`);
|
|
275
|
+
return { exitCode: 1 };
|
|
276
|
+
}
|
|
217
277
|
err(`port :443 is held by pid ${foreign}, which is not this workspace's gateway.\n` +
|
|
218
278
|
`Free it first (stop that process), then \`lensmcp gateway start\`.`);
|
|
219
279
|
return { exitCode: 1 };
|
|
@@ -247,6 +307,19 @@ function runGateway(ctx, args, out, err) {
|
|
|
247
307
|
return { exitCode: 0 };
|
|
248
308
|
};
|
|
249
309
|
const stop = () => {
|
|
310
|
+
// If THIS workspace registered into a shared daemon (P4), UNREGISTER from it instead of killing a
|
|
311
|
+
// gateway it never spawned — the daemon reaps this workspace's services + dashboard.
|
|
312
|
+
if (existsSync(registeredMarker)) {
|
|
313
|
+
let wsKey = cfg.key;
|
|
314
|
+
try {
|
|
315
|
+
wsKey = JSON.parse(readFileSync(registeredMarker, 'utf8')).wsKey ?? cfg.key;
|
|
316
|
+
}
|
|
317
|
+
catch { /* fall back to cfg.key */ }
|
|
318
|
+
const res = daemonRequest('POST', '/unregister', { wsKey });
|
|
319
|
+
rmFile(registeredMarker);
|
|
320
|
+
out(res && res.status === 200 ? `unregistered '${wsKey}' from the shared gateway daemon.` : 'unregister sent (the daemon may already be gone).');
|
|
321
|
+
return { exitCode: 0 };
|
|
322
|
+
}
|
|
250
323
|
// Reality-based: kill the tracked pid OR an orphaned gateway the pid file lost track of (so `stop`
|
|
251
324
|
// works even after the desync — the case where the old CLI reported "not running" but :443 was held).
|
|
252
325
|
const { pid, healed } = reconcileGateway(pidFile, cwd);
|
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.13",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "David Antoon",
|
|
8
8
|
"email": "davidmantoon@gmail.com"
|