lensmcp 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bundled/main.js CHANGED
@@ -11,6 +11,9 @@ function __decorate(decorators, target, key, desc) {
11
11
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
12
12
  return c > 3 && r && Object.defineProperty(target, key, r), r;
13
13
  }
14
+ function __metadata(metadataKey, metadataValue) {
15
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
16
+ }
14
17
 
15
18
  // servers/lensmcp-mcp/dist/main.js
16
19
  import "reflect-metadata";
@@ -20232,10 +20235,145 @@ RenderApp = __decorate([
20232
20235
  })
20233
20236
  ], RenderApp);
20234
20237
 
20238
+ // servers/lensmcp-mcp/dist/setup-gate/setup.app.js
20239
+ import { App as App21 } from "@frontmcp/sdk";
20240
+
20241
+ // servers/lensmcp-mcp/dist/setup-gate/setup-status.tool.js
20242
+ import { Tool as Tool24, ToolContext as ToolContext24 } from "@frontmcp/sdk";
20243
+
20244
+ // servers/lensmcp-mcp/dist/setup-gate/state.js
20245
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
20246
+ import { dirname as dirname3, join as join3 } from "node:path";
20247
+ import { fileURLToPath } from "node:url";
20248
+ function lensmcpDir() {
20249
+ const eventFile = process.env["LENSMCP_EVENT_FILE"];
20250
+ if (eventFile)
20251
+ return dirname3(eventFile);
20252
+ return join3(process.cwd(), ".lensmcp");
20253
+ }
20254
+ function readSetupState() {
20255
+ try {
20256
+ const raw = readFileSync3(join3(lensmcpDir(), "setup-state.json"), "utf8");
20257
+ const parsed = JSON.parse(raw);
20258
+ return typeof parsed?.version === "string" ? parsed : void 0;
20259
+ } catch {
20260
+ return void 0;
20261
+ }
20262
+ }
20263
+ function installedVersion() {
20264
+ let dir = dirname3(fileURLToPath(import.meta.url));
20265
+ for (let i = 0; i < 6; i++) {
20266
+ const pkg = join3(dir, "package.json");
20267
+ if (existsSync5(pkg)) {
20268
+ try {
20269
+ const version2 = JSON.parse(readFileSync3(pkg, "utf8")).version;
20270
+ if (version2)
20271
+ return version2;
20272
+ } catch {
20273
+ }
20274
+ }
20275
+ const parent = dirname3(dir);
20276
+ if (parent === dir)
20277
+ break;
20278
+ dir = parent;
20279
+ }
20280
+ return "0.0.0";
20281
+ }
20282
+ function checkReadiness() {
20283
+ const installed = installedVersion();
20284
+ const state2 = readSetupState();
20285
+ if (!state2)
20286
+ return { ready: false, installedVersion: installed, reason: "never-setup" };
20287
+ if (state2.version !== installed) {
20288
+ return { ready: false, installedVersion: installed, setupVersion: state2.version, reason: "version-mismatch" };
20289
+ }
20290
+ return { ready: true, installedVersion: installed, setupVersion: state2.version };
20291
+ }
20292
+ function gateDisabled() {
20293
+ const flag = process.env["LENSMCP_NO_SETUP_GATE"];
20294
+ return flag === "1" || flag === "true";
20295
+ }
20296
+ function setupRequiredMessage(r) {
20297
+ const last = r.reason === "version-mismatch" && r.setupVersion ? `last set up for v${r.setupVersion}` : "not set up on this machine yet";
20298
+ return `LensMCP is not ready: ${last}, but v${r.installedVersion} is installed. Ask the user to run \`lensmcp setup\` once in their terminal \u2014 it installs the plugin wiring and trusts the dev CA + /etc/hosts (sudo prompts inline; idempotent) \u2014 then retry. Call the \`setup.status\` tool any time to re-check.`;
20299
+ }
20300
+
20301
+ // servers/lensmcp-mcp/dist/setup-gate/setup-status.tool.js
20302
+ var SetupStatusTool = class SetupStatusTool2 extends ToolContext24 {
20303
+ async execute() {
20304
+ const readiness = checkReadiness();
20305
+ return {
20306
+ ready: readiness.ready,
20307
+ installedVersion: readiness.installedVersion,
20308
+ setupVersion: readiness.setupVersion ?? null,
20309
+ reason: readiness.reason ?? null,
20310
+ gateDisabled: gateDisabled(),
20311
+ message: readiness.ready ? `LensMCP is set up for v${readiness.installedVersion}. The lens is open.` : setupRequiredMessage(readiness),
20312
+ fix: readiness.ready ? null : "lensmcp setup"
20313
+ };
20314
+ }
20315
+ };
20316
+ SetupStatusTool = __decorate([
20317
+ Tool24({
20318
+ name: "setup.status",
20319
+ description: "Report whether LensMCP is set up for the installed version on this machine. Always available (exempt from the setup-gate) so the agent can diagnose. If `ready` is false, ask the user to run `lensmcp setup`.",
20320
+ inputSchema: {}
20321
+ })
20322
+ ], SetupStatusTool);
20323
+ var setup_status_tool_default = SetupStatusTool;
20324
+
20325
+ // servers/lensmcp-mcp/dist/setup-gate/gate.plugin.js
20326
+ import { Plugin, ToolHook } from "@frontmcp/sdk";
20327
+ var { Will } = ToolHook;
20328
+ var EXEMPT_TOOLS = /* @__PURE__ */ new Set(["setup.status", "lensmcp.session"]);
20329
+ var SetupGatePlugin = class SetupGatePlugin2 {
20330
+ enforceSetup(ctx) {
20331
+ if (gateDisabled())
20332
+ return;
20333
+ let readiness;
20334
+ try {
20335
+ readiness = checkReadiness();
20336
+ } catch {
20337
+ return;
20338
+ }
20339
+ if (!readiness.ready)
20340
+ ctx.fail(new Error(setupRequiredMessage(readiness)));
20341
+ }
20342
+ };
20343
+ __decorate([
20344
+ Will("execute", {
20345
+ priority: 1e4,
20346
+ filter: (ctx) => !EXEMPT_TOOLS.has(ctx.toolName ?? "")
20347
+ }),
20348
+ __metadata("design:type", Function),
20349
+ __metadata("design:paramtypes", [Object]),
20350
+ __metadata("design:returntype", void 0)
20351
+ ], SetupGatePlugin.prototype, "enforceSetup", null);
20352
+ SetupGatePlugin = __decorate([
20353
+ Plugin({
20354
+ name: "lensmcp-setup-gate",
20355
+ description: "Blocks lens tools until the workspace has run `lensmcp setup` for the installed lensmcp version (after install or upgrade). Fails open on error."
20356
+ })
20357
+ ], SetupGatePlugin);
20358
+
20359
+ // servers/lensmcp-mcp/dist/setup-gate/setup.app.js
20360
+ var SetupApp = class SetupApp2 {
20361
+ };
20362
+ SetupApp = __decorate([
20363
+ App21({
20364
+ id: "lensmcp-setup",
20365
+ name: "LensMCP Setup Gate",
20366
+ description: "Readiness gate: blocks lens tools until the workspace has run `lensmcp setup`, plus the always-open `setup.status` diagnostic.",
20367
+ tools: [setup_status_tool_default],
20368
+ plugins: [SetupGatePlugin]
20369
+ })
20370
+ ], SetupApp);
20371
+
20235
20372
  // servers/lensmcp-mcp/dist/main.js
20236
20373
  var { session, providers } = createLensSession();
20237
20374
  var transport = process.env["LENSMCP_TRANSPORT"] ?? "stdio";
20238
20375
  var apps = [
20376
+ SetupApp,
20239
20377
  MetaApp,
20240
20378
  AgentApp,
20241
20379
  EventsApp,
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;AAoDD,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAoChE"}
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;AAiED,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAwChE"}
package/lib/cli.js CHANGED
@@ -9,6 +9,13 @@ Usage:
9
9
  lensmcp <command> [options]
10
10
 
11
11
  Commands:
12
+ setup [--cwd <dir>] [--skip-format] [--register-host-config <mode>]
13
+ First-time bootstrap — runs \`install\` then \`trust\` in
14
+ one idempotent pass: plugin + .mcp.json + nx config +
15
+ vite/nest wiring, then the dev CA (System keychain),
16
+ /etc/hosts (both families) + DNS flush. sudo self-prompts
17
+ only for the steps that need it — run in a real terminal.
18
+ Then \`lensmcp gateway start\`.
12
19
  mcp [--cwd <dir>] [--transport stdio|http] [--port <n>]
13
20
  Launch the LensMCP MCP server. Default transport is
14
21
  stdio (set LENSMCP_TRANSPORT=http or pass --transport
@@ -38,6 +45,12 @@ Commands:
38
45
  Install LensMCP into the host Nx workspace at --cwd
39
46
  (current directory by default). Delegates to
40
47
  \`nx g @lensmcp/nx-plugin:init\`. Idempotent.
48
+ trust [--cwd <dir>] [--project <p>] [--target <t>]
49
+ Make the HTTPS dev gateway trusted: mint + trust the local
50
+ dev CA, write /etc/hosts (127.0.0.1 + ::1) for every
51
+ cluster host (+ lensmcp.local), flush DNS. Wraps the
52
+ auto-discovered \`@lensmcp/cluster:trust\` target.
53
+ Idempotent; sudo prompts inline — run in a terminal.
41
54
  doctor [--cwd <dir>] [--json]
42
55
  Diagnose the install: Node, workspace + plugin,
43
56
  .mcp.json server entry, MCP bundle, per-project
@@ -65,6 +78,8 @@ export async function runCli(ctx) {
65
78
  return { exitCode: 0 };
66
79
  }
67
80
  switch (command) {
81
+ case 'setup':
82
+ return runSetup(ctx, rest, out, err);
68
83
  case 'mcp':
69
84
  return runMcp(ctx, rest, out, err);
70
85
  case 'dashboard':
@@ -75,6 +90,8 @@ export async function runCli(ctx) {
75
90
  return runBridge(ctx, rest, out, err);
76
91
  case 'install':
77
92
  return runInstall(ctx, rest, out, err);
93
+ case 'trust':
94
+ return runTrust(ctx, rest, out, err);
78
95
  case 'doctor':
79
96
  return runDoctor(ctx, rest, out);
80
97
  case 'rollout':
@@ -261,9 +278,10 @@ function runGateway(ctx, args, out, err) {
261
278
  return { exitCode: 2 };
262
279
  }
263
280
  }
264
- /** Find a project + target whose executor is `@lensmcp/cluster:gateway`. An explicit
265
- * `project`/`target` short-circuits the scan; otherwise we read each project.json. */
266
- function findGatewayTarget(cwd, project, target) {
281
+ /** Scan every project.json for the first project carrying a target whose executor
282
+ * is `executor`. An explicit project+target short-circuits the scan; a partial
283
+ * override fills the missing half from the match. */
284
+ function scanExecutorTarget(cwd, executor, project, target) {
267
285
  if (project && target)
268
286
  return { project, target };
269
287
  const files = walkProjectFiles(cwd, (n) => n === 'project.json');
@@ -277,14 +295,30 @@ function findGatewayTarget(cwd, project, target) {
277
295
  }
278
296
  const name = pj.name ?? basename(dirname(f));
279
297
  for (const [t, def] of Object.entries(pj.targets ?? {})) {
280
- if (def.executor === '@lensmcp/cluster:gateway') {
298
+ if (def.executor === executor) {
281
299
  return { project: project ?? name, target: target ?? t };
282
300
  }
283
301
  }
284
302
  }
303
+ return undefined;
304
+ }
305
+ /** Find a project + target whose executor is `@lensmcp/cluster:gateway`. */
306
+ function findGatewayTarget(cwd, project, target) {
307
+ const found = scanExecutorTarget(cwd, '@lensmcp/cluster:gateway', project, target);
308
+ if (found)
309
+ return found;
285
310
  // Sensible fallback used by most workspaces (tetros: tools/gateway → `gateway:serve`).
286
311
  return project || target ? { project: project ?? 'gateway', target: target ?? 'serve' } : undefined;
287
312
  }
313
+ /** Find a project + target whose executor is `@lensmcp/cluster:trust`. Falls back
314
+ * unconditionally to `gateway:trust` — the conventional home the init generator
315
+ * scaffolds — since trust is idempotent and safe to attempt anywhere. */
316
+ function findTrustTarget(cwd, project, target) {
317
+ return (scanExecutorTarget(cwd, '@lensmcp/cluster:trust', project, target) ?? {
318
+ project: project ?? 'gateway',
319
+ target: target ?? 'trust',
320
+ });
321
+ }
288
322
  function readPid(pidFile) {
289
323
  try {
290
324
  const n = Number(readFileSync(pidFile, 'utf8').trim());
@@ -384,6 +418,96 @@ function runInstall(ctx, args, out, err) {
384
418
  });
385
419
  return { exitCode: result.status ?? 1 };
386
420
  }
421
+ /**
422
+ * `lensmcp trust` — make the HTTPS dev gateway trusted in real browsers: mint +
423
+ * trust the local dev CA, write `/etc/hosts` (both `127.0.0.1` and `::1`) for
424
+ * every cluster host plus `lensmcp.local`, and flush DNS. Delegates to the
425
+ * auto-discovered `@lensmcp/cluster:trust` target. The executor self-escalates
426
+ * with `sudo` per privileged step and skips each when already satisfied, so this
427
+ * is idempotent — but it needs a real terminal so sudo can prompt.
428
+ */
429
+ function runTrust(ctx, args, out, err) {
430
+ const opts = parseFlags(args, { string: ['--cwd', '--project', '--target'] });
431
+ const cwd = resolve(stringFlag(opts.flags['--cwd']) ?? ctx.cwd);
432
+ if (!existsSync(join(cwd, 'nx.json'))) {
433
+ err(`No nx.json found at ${cwd}.`);
434
+ err('Run `lensmcp trust` from inside a Nx workspace (after `lensmcp install`).');
435
+ return { exitCode: 1 };
436
+ }
437
+ const nxBin = findNxBinary(cwd);
438
+ if (!nxBin) {
439
+ err('Could not find the `nx` binary in the workspace. Install Nx first: `yarn add -D nx`.');
440
+ return { exitCode: 1 };
441
+ }
442
+ const target = findTrustTarget(cwd, stringFlag(opts.flags['--project']), stringFlag(opts.flags['--target']));
443
+ if (!isInteractive()) {
444
+ out(' note: trust runs `sudo` for the CA + /etc/hosts — run it in a real terminal if a step is needed.');
445
+ }
446
+ out(`> ${nxBin} run ${target.project}:${target.target} (sudo prompts inline for the CA + /etc/hosts)`);
447
+ const result = spawnSync(nxBin, ['run', `${target.project}:${target.target}`], {
448
+ cwd,
449
+ stdio: 'inherit',
450
+ env: { ...process.env, ...(ctx.env ?? {}) },
451
+ });
452
+ return { exitCode: result.status ?? 1 };
453
+ }
454
+ /**
455
+ * Write the setup-state marker the MCP setup-gate reads to decide readiness.
456
+ * The shape MUST match `servers/lensmcp-mcp/src/setup-gate/state.ts`
457
+ * (`SetupState`). Machine-local (`.lensmcp/` is gitignored): it records the
458
+ * lensmcp version setup completed for, so an upgrade re-arms the gate until
459
+ * `setup` runs again.
460
+ */
461
+ function writeSetupState(cwd) {
462
+ ensureLensmcpDir(cwd);
463
+ const state = {
464
+ version: readCliVersion(),
465
+ completedAt: new Date().toISOString(),
466
+ steps: { install: true, trust: true },
467
+ };
468
+ try {
469
+ writeFileSync(join(cwd, '.lensmcp', 'setup-state.json'), `${JSON.stringify(state, null, 2)}\n`);
470
+ }
471
+ catch {
472
+ /* non-fatal: the gate just treats the workspace as not-yet-set-up */
473
+ }
474
+ }
475
+ /**
476
+ * `lensmcp setup` — first-time bootstrap. Runs `install` (plugin + .mcp.json +
477
+ * nx config + vite/nest wiring, no sudo) then `trust` (dev CA → keychain,
478
+ * /etc/hosts, DNS flush, sudo). Both steps are idempotent, so re-running is safe.
479
+ * Flags are forwarded to whichever sub-command understands them (`--skip-format`,
480
+ * `--register-host-config` → install; `--project`/`--target` → trust).
481
+ */
482
+ function runSetup(ctx, args, out, err) {
483
+ const opts = parseFlags(args, { string: ['--cwd'] });
484
+ const cwd = resolve(stringFlag(opts.flags['--cwd']) ?? ctx.cwd);
485
+ const subCtx = { ...ctx, cwd };
486
+ out('lensmcp setup — first-time bootstrap (install → trust)');
487
+ out('');
488
+ out('[1/2] install — plugin, .mcp.json, nx config, vite/nest wiring');
489
+ const installRes = runInstall(subCtx, args, out, err);
490
+ if (installRes.exitCode !== 0) {
491
+ err('');
492
+ err('setup: the install step failed — fix the above and re-run `lensmcp setup`.');
493
+ return installRes;
494
+ }
495
+ out('');
496
+ out('[2/2] trust — dev CA → System keychain, /etc/hosts, DNS flush (sudo)');
497
+ const trustRes = runTrust(subCtx, args, out, err);
498
+ if (trustRes.exitCode !== 0) {
499
+ err('');
500
+ err('setup: the trust step failed — run `lensmcp setup` directly in a terminal so sudo can prompt.');
501
+ return trustRes;
502
+ }
503
+ // Both steps succeeded → mark the workspace set up for this version (opens the MCP gate).
504
+ writeSetupState(cwd);
505
+ out('');
506
+ out('✓ setup complete.');
507
+ out(' next: lensmcp gateway start (the :443 front door — needs sudo)');
508
+ out(' verify: lensmcp doctor');
509
+ return { exitCode: 0 };
510
+ }
387
511
  function runDoctor(ctx, args, out) {
388
512
  const opts = parseFlags(args, { string: ['--cwd'], boolean: ['--json'] });
389
513
  const cwd = resolve(stringFlag(opts.flags['--cwd']) ?? ctx.cwd);
@@ -697,6 +821,11 @@ function ensureLensmcpDir(cwd) {
697
821
  if (!existsSync(dir))
698
822
  mkdirSync(dir, { recursive: true });
699
823
  }
824
+ /** `sudo` can only prompt for a password when both stdin and stdout are TTYs.
825
+ * Used to warn (not block) before the trust step's privileged sub-commands. */
826
+ function isInteractive() {
827
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
828
+ }
700
829
  /** Narrow a flag value to string-or-undefined, treating booleans as absent. */
701
830
  function stringFlag(v) {
702
831
  if (v === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lensmcp",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
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.9.0",
5
+ "version": "1.11.0",
6
6
  "author": {
7
7
  "name": "David Antoon",
8
8
  "email": "davidmantoon@gmail.com"
@@ -25,9 +25,11 @@ After running it:
25
25
  1. Report the dashboard URL the script printed.
26
26
  2. **Binding :443 needs privilege.** If the log shows `EACCES`/permission denied, tell
27
27
  the user to either run the gateway target with `sudo`, or that ports <1024 require
28
- elevation on macOS — and that `nx run gateway:trust` must be run once (sudo) to set up
29
- the `*.local` hosts + TLS CA. Check `.lensmcp/gateway.log` for the real error.
30
- 3. If the user has not run `gateway:trust` yet, mention it (one-time per machine).
28
+ elevation on macOS — and that `lensmcp setup` (install + trust) or `lensmcp trust`
29
+ must be run once (sudo) to set up the `*.local` hosts + TLS CA. Check
30
+ `.lensmcp/gateway.log` for the real error.
31
+ 3. If the user has not run `lensmcp setup` / `lensmcp trust` yet, mention it (one-time
32
+ per machine).
31
33
 
32
34
  Do **not** run a service's `serve`/`serve-hmr` manually — the gateway owns each service's
33
35
  lifecycle. This one command is the entry point.
@@ -10,6 +10,15 @@ server and the dashboard both read that file. **Project isolation is automatic**
10
10
  server is launched with `--cwd ${CLAUDE_PROJECT_DIR}`, so it always reads the flows of the
11
11
  project Claude Code is open in. Different project → different event file → different flows.
12
12
 
13
+ ## First: is the lens set up?
14
+
15
+ LensMCP gates its tools until the workspace has been set up **once per machine** (and again
16
+ after a lensmcp upgrade). If a tool fails with a **"LensMCP is not ready — run `lensmcp setup`"**
17
+ message — or if `setup.status` reports `ready: false` — do **not** keep retrying. Tell the user
18
+ to run **`lensmcp setup`** in their terminal (it wires the plugin + trusts the dev CA/hosts;
19
+ sudo prompts inline; idempotent), then continue. The `setup.status` tool and the session
20
+ bootstrap stay available even while the gate is closed.
21
+
13
22
  ## The loop
14
23
 
15
24
  1. **Start** the cluster + lens: `/lensmcp:start` (gateway up, all hosts routed, dashboard