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 CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AAMA,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"}
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 { basename, dirname, join, resolve } from 'node:path';
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 free of any lens gateway — but if a FOREIGN process holds it, spawning would just
214
- // EADDRINUSE-crash and re-create the orphan mess. Surface it instead of stacking a doomed child.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lensmcp",
3
- "version": "1.16.12",
3
+ "version": "1.16.13",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -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.12",
5
+ "version": "1.16.13",
6
6
  "author": {
7
7
  "name": "David Antoon",
8
8
  "email": "davidmantoon@gmail.com"