@lensmcp/nx-plugin 1.23.13 → 1.24.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/README.md CHANGED
@@ -48,7 +48,7 @@ This is also run for you by `lensmcp install` (the [`lensmcp` CLI](https://githu
48
48
 
49
49
  | Executor | Purpose |
50
50
  | --- | --- |
51
- | `agent-dev` | Start a project under the LensMCP lens — runs the dev server alongside a Chrome capture sidecar and the MCP server so agents observe the live app. |
51
+ | `agent-dev` | Start a project under the LensMCP lens — runs the dev server alongside a Chrome capture sidecar, and attaches the workspace's shared MCP server (starting it if needed) so agents observe the live app. |
52
52
  | `agent-build` | Run an instrumented production build that produces a bundle report and updates the baseline. |
53
53
  | `agent-verify` | Deterministic verification loop — typecheck, lint, then build — with a per-run report under `.lensmcp/verifications/`. |
54
54
 
@@ -65,6 +65,7 @@ Runs a host project (`vite-react`, `nestjs`, or `nextjs`) and attaches the lens.
65
65
  | `headless` | `boolean` | `true` | Run the Chrome sidecar headless. |
66
66
  | `port` | `number` | — | Override the dev port. Defaults to the project's `vite.config.ts` setting or `5173`. |
67
67
  | `openUrl` | `string` | — | Override the URL Chrome navigates to. |
68
+ | `mcp` | `"shared"` \| `"embedded"` | `LENSMCP_MCP_MODE`, then `.lensmcp/config.json` `mcp.mode`, then `shared` | `shared` attaches the workspace's one shared MCP server (its own port from `.lensmcp/config.json`, started if it is not up) and starts no server of its own; `embedded` runs agent-dev's own server on `LENSMCP_PORT` (default `3000`). |
68
69
 
69
70
  ### `agent-build`
70
71
 
@@ -1,3 +1,4 @@
1
+ import { type ChildProcess } from 'node:child_process';
1
2
  import type { ExecutorContext } from '@nx/devkit';
2
3
  import type { AgentDevExecutorSchema } from './schema';
3
4
  interface ExecutorResult {
@@ -10,7 +11,9 @@ interface ExecutorResult {
10
11
  * already wired by `setup-vite`).
11
12
  * 2. (Optional) a Chrome sidecar pointed at the dev URL so CDP
12
13
  * collectors see what the user sees.
13
- * 3. The LensMCP MCP server, so an agent can connect.
14
+ * 3. The LensMCP MCP side, so an agent can connect — in `shared` mode (the
15
+ * default) the workspace's ONE shared server, started if it is not up; in
16
+ * `embedded` mode a server of its own (see `startMcp`).
14
17
  *
15
18
  * The MCP server and the browser-capture runner are both spawned from the
16
19
  * self-contained `lensmcp` CLI bundle (`bundled/main.js` +
@@ -23,15 +26,63 @@ interface ExecutorResult {
23
26
  * what's happening. Ctrl-C triggers a clean teardown.
24
27
  */
25
28
  export default function agentDevExecutor(options: AgentDevExecutorSchema, context: ExecutorContext): Promise<ExecutorResult>;
29
+ /**
30
+ * Signal a child ONLY if its spawn produced a process (issues/closed/test-infra-04 — the same defect the
31
+ * cluster gateway's lens children had).
32
+ *
33
+ * A `ChildProcess` whose spawn FAILED — its cwd is gone, its binary is not there — has no pid, yet keeps
34
+ * its native handle until the `'error'` tick, and `kill()` on that handle reaches `kill(0, signal)`: this
35
+ * executor's OWN process group (agent-dev, the nx runner above it, the terminal job). One missing project
36
+ * root fails every child in the same run, and the first failure's `shutdown()` used to signal the rest.
37
+ */
38
+ export declare function signalChild(child: ChildProcess, signal: NodeJS.Signals): boolean;
39
+ /** How a session serves MCP — the CLI's `McpMode` (`libs/cli/src/lib/mcp-mode.ts`). */
40
+ export type AgentDevMcpMode = 'shared' | 'embedded';
41
+ /**
42
+ * The MCP mode in force, by the CLI's precedence: the target's `mcp` option (in place of `--mcp`) >
43
+ * `LENSMCP_MCP_MODE` > `.lensmcp/config.json` `mcp.mode` > `shared`. A value that is neither mode falls
44
+ * through to the next layer, as there. Restated rather than imported: `lensmcp` depends on this
45
+ * package, so this package cannot depend on it (`resolveMcpModeWithSource`, `mcp-mode.ts`).
46
+ */
47
+ export declare function resolveAgentDevMcpMode(option: string | undefined, env: NodeJS.ProcessEnv, workspaceRoot: string): AgentDevMcpMode;
48
+ /** What one line of the shim's stderr says about the shared server. */
49
+ export type ShimReport =
50
+ /** `shim → workspace "<key>" (<root>) → <url>` — the workspace it resolved, and its server's URL. */
51
+ {
52
+ kind: 'scope';
53
+ url: string;
54
+ }
55
+ /** `shim: shared server <outcome> (<n>ms)` — a server answered as this workspace's and holds our bearer. */
56
+ | {
57
+ kind: 'attached';
58
+ outcome: string;
59
+ }
60
+ /** `shared MCP server unavailable (<reason>) — falling back …` — it will not attach, and says why. */
61
+ | {
62
+ kind: 'unavailable';
63
+ reason: string;
64
+ };
65
+ /**
66
+ * Read one line of the shim's stderr (`servers/lensmcp-mcp/src/shim.ts`, its `log` calls). Its human
67
+ * report is the only one it makes, so this is pinned against the REAL shim by `shared-mcp.e2e.spec.ts`.
68
+ */
69
+ export declare function readShimLine(line: string): ShimReport | undefined;
26
70
  /**
27
71
  * May automatic capture point a browser at this URL?
28
72
  *
29
73
  * Loopback only, unless `LENSMCP_CAPTURE_ALLOW_REMOTE=1` says otherwise. `agent-dev` starts capture
30
74
  * by DEFAULT, so `openUrl` is the one place a workspace config decides what a browser on the
31
- * developer's machine renders — and, with the sandbox restored (issues/codex-security/60), a target
75
+ * developer's machine renders — and, with the sandbox restored (issues/closed/codex-security-60), a target
32
76
  * on the open internet is a decision worth stating rather than inheriting.
33
77
  *
34
78
  * Anything that does not parse, and any scheme other than http/https, is not local.
35
79
  */
80
+ /**
81
+ * The HTTP MCP server this executor attaches (shared) or starts (embedded) answers only callers
82
+ * presenting this account's bearer (issues/closed/codex-security-03, 42). The stdio `lensmcp mcp` shim
83
+ * finds it on its own; a client configured with a `type: "http"` URL has to send it — say where it is,
84
+ * never what it is.
85
+ */
86
+ export declare function mcpAuthHint(env?: NodeJS.ProcessEnv): string;
36
87
  export declare function isLocalCaptureTarget(url: string, env?: NodeJS.ProcessEnv): boolean;
37
88
  export {};
@@ -1,4 +1,4 @@
1
- "use strict";var L=Object.defineProperty;var P=(t,o)=>L(t,"name",{value:o,configurable:!0});var M=Object.defineProperty,d=P((t,o)=>M(t,"name",{value:o,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=agentDevExecutor,exports.isLocalCaptureTarget=isLocalCaptureTarget;const node_child_process_1=require("node:child_process"),node_fs_1=require("node:fs"),node_path_1=require("node:path"),lens_frontend_1=require("../../lens-frontend");async function agentDevExecutor(t,o){const e={kind:t.kind??"vite-react",chrome:t.chrome??!0,headless:t.headless??!0,node:t.node??!0,devTarget:t.devTarget,projectRoot:t.projectRoot,port:t.port,openUrl:t.openUrl,devCommand:t.devCommand,wsPort:t.wsPort};if(e.kind!=="vite-react"&&e.kind!=="nestjs"&&e.kind!=="imported")return console.error(`[agent-dev] supported kinds are "vite-react", "nestjs", "imported" (got "${e.kind}").`),{success:!1};const r=o.projectName,p=e.projectRoot?(0,node_path_1.resolve)(o.root,e.projectRoot):r&&o.projectsConfigurations?(0,node_path_1.resolve)(o.root,o.projectsConfigurations.projects[r]?.root??"."):o.root;if(e.kind==="nestjs")return runNestjs(p,o);if(e.kind==="imported")return runImported(p,o,{devCommand:e.devCommand,chrome:e.chrome,headless:e.headless,openUrl:e.openUrl,wsPort:e.wsPort??5747,node:e.node});const l=(0,lens_frontend_1.findMcpBundle)(o.root);l||console.warn("[agent-dev] No lensmcp-mcp bundle found \u2014 skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/main.js`).");const h=[],c=new Map;let s=!1;function u(n,i,v,f,w=!1,a){const C=(0,node_child_process_1.spawn)(i,v,{cwd:p,env:{...process.env,...f??{}},stdio:"inherit"});return h.push(C),C.on("exit",(S,E)=>{if(!s){if(w){const N=c.get(n)??0;if(a&&N<a.max){c.set(n,N+1),console.warn(`[agent-dev] ${n} exited (code=${S} sig=${E}); respawning in ${Math.round(a.delayMs/1e3)}s (${N+1}/${a.max}).`),setTimeout(()=>{s||u(n,i,v,f,w,a)},a.delayMs).unref?.();return}console.warn(`[agent-dev] ${n} exited (code=${S} sig=${E}); continuing.`);return}console.warn(`[agent-dev] ${n} exited unexpectedly (code=${S} sig=${E}); shutting down others.`),m()}}),C}P(u,"l"),d(u,"spawnChild");function m(){if(!s){s=!0;for(const n of h)try{n.kill("SIGTERM")}catch{}}}P(m,"u"),d(m,"shutdown"),process.on("SIGINT",m),process.on("SIGTERM",m);const g=process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(o.root,".lensmcp","events.jsonl");try{(0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(g),{recursive:!0});const n=(0,node_fs_1.statSync)(g,{throwIfNoEntry:!1});!n||Date.now()-n.mtimeMs>6e4?(0,node_fs_1.writeFileSync)(g,""):console.log("[agent-dev] event bridge is active (written <60s ago) \u2014 appending, not truncating.")}catch{}const _={LENSMCP_EVENT_FILE:g};console.log(`[agent-dev] event bridge \u2192 ${g}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:p,workspaceRoot:o.root,...r?{project:r}:{},eventFile:g,...e.port!==void 0?{port:e.port}:{},chrome:e.chrome,headless:e.headless,...e.openUrl?{openUrl:e.openUrl}:{},log:d(n=>console.log(n),"log")},u)}catch(n){return console.error(n.message),m(),{success:!1}}return l&&(console.log(`[agent-dev] starting LensMCP MCP server (${l})`),u("lensmcp-mcp",process.execPath,[l],{..._,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:process.env.LENSMCP_PORT??"3000"})),new Promise(n=>{const i=d(()=>{s?Promise.allSettled(h.map(v=>new Promise(f=>{v.exitCode!==null?f():v.on("exit",()=>f())}))).then(()=>n({success:!0})):setTimeout(i,500)},"tick");i()})}P(agentDevExecutor,"agentDevExecutor"),d(agentDevExecutor,"agentDevExecutor");async function runNestjs(t,o){const e=o.projectName,r=(e?o.projectsConfigurations?.projects[e]?.targets?.build:void 0)?.options?.outputPath,p=[...typeof r=="string"?[(0,node_path_1.join)(o.root,r,"main.js")]:[],(0,node_path_1.join)(t,"dist","main.js"),(0,node_path_1.join)(o.root,"dist",(0,node_path_1.relative)(o.root,t),"main.js")],l=p.find(c=>(0,node_fs_1.existsSync)(c));if(!l)return console.error(`[agent-dev] No built NestJS entry found. Looked in:
2
- `+p.map(c=>` \u2022 ${c}`).join(`
1
+ "use strict";var j=Object.defineProperty;var d=(e,n)=>j(e,"name",{value:n,configurable:!0});var T=Object.defineProperty,t=d((e,n)=>T(e,"name",{value:n,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=agentDevExecutor,exports.signalChild=signalChild,exports.resolveAgentDevMcpMode=resolveAgentDevMcpMode,exports.readShimLine=readShimLine,exports.mcpAuthHint=mcpAuthHint,exports.isLocalCaptureTarget=isLocalCaptureTarget;const node_child_process_1=require("node:child_process"),node_fs_1=require("node:fs"),node_path_1=require("node:path"),node_readline_1=require("node:readline"),lens_frontend_1=require("../../lens-frontend");async function agentDevExecutor(e,n){const o={kind:e.kind??"vite-react",chrome:e.chrome??!0,headless:e.headless??!0,node:e.node??!0,devTarget:e.devTarget,projectRoot:e.projectRoot,port:e.port,openUrl:e.openUrl,devCommand:e.devCommand,wsPort:e.wsPort,mcp:resolveAgentDevMcpMode(e.mcp,process.env,n.root)};if(o.kind!=="vite-react"&&o.kind!=="nestjs"&&o.kind!=="imported")return console.error(`[agent-dev] supported kinds are "vite-react", "nestjs", "imported" (got "${o.kind}").`),{success:!1};const r=n.projectName,g=o.projectRoot?(0,node_path_1.resolve)(n.root,o.projectRoot):r&&n.projectsConfigurations?(0,node_path_1.resolve)(n.root,n.projectsConfigurations.projects[r]?.root??"."):n.root;if(o.kind==="nestjs")return runNestjs(g,n);if(o.kind==="imported")return runImported(g,n,{devCommand:o.devCommand,chrome:o.chrome,headless:o.headless,openUrl:o.openUrl,wsPort:o.wsPort??5747,node:o.node,mcp:o.mcp});const p=(0,lens_frontend_1.findMcpBundle)(n.root);p||console.warn("[agent-dev] No lensmcp-mcp bundle found \u2014 skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/main.js`).");const u=[],c=new Map;let a=!1;function v(i,h,C,s,m=!1,f){const P=(0,node_child_process_1.spawn)(h,C,{cwd:g,env:{...process.env,...s??{}},stdio:"inherit"});return u.push(P),onSpawnFailure(P,i,()=>{m||l()}),P.on("exit",(E,M)=>{if(!a){if(m){const w=c.get(i)??0;if(f&&w<f.max){c.set(i,w+1),console.warn(`[agent-dev] ${i} exited (code=${E} sig=${M}); respawning in ${Math.round(f.delayMs/1e3)}s (${w+1}/${f.max}).`),setTimeout(()=>{a||v(i,h,C,s,m,f)},f.delayMs).unref?.();return}console.warn(`[agent-dev] ${i} exited (code=${E} sig=${M}); continuing.`);return}console.warn(`[agent-dev] ${i} exited unexpectedly (code=${E} sig=${M}); shutting down others.`),l()}}),P}d(v,"g"),t(v,"spawnChild");function l(){if(!a){a=!0;for(const i of u)signalChild(i,"SIGTERM")}}d(l,"p"),t(l,"shutdown"),process.on("SIGINT",l),process.on("SIGTERM",l);const S=process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(n.root,".lensmcp","events.jsonl");try{(0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(S),{recursive:!0});const i=(0,node_fs_1.statSync)(S,{throwIfNoEntry:!1});!i||Date.now()-i.mtimeMs>6e4?(0,node_fs_1.writeFileSync)(S,""):console.log("[agent-dev] event bridge is active (written <60s ago) \u2014 appending, not truncating.")}catch{}console.log(`[agent-dev] event bridge \u2192 ${S}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:g,workspaceRoot:n.root,...r?{project:r}:{},eventFile:S,...o.port!==void 0?{port:o.port}:{},chrome:o.chrome,headless:o.headless,...o.openUrl?{openUrl:o.openUrl}:{},log:t(i=>console.log(i),"log")},v)}catch(i){return console.error(i.message),l(),{success:!1}}return p&&startMcp({mcpBundle:p,workspaceRoot:n.root,eventFile:S,mode:o.mcp,spawnChild:t((i,h,C,s)=>v(i,h,C,s),"spawnChild"),track:t(i=>{u.push(i)},"track")}),new Promise(i=>{const h=t(()=>{a?Promise.allSettled(u.map(exited)).then(()=>i({success:!0})):setTimeout(h,500)},"tick");h()})}d(agentDevExecutor,"agentDevExecutor"),t(agentDevExecutor,"agentDevExecutor");async function runNestjs(e,n){const o=n.projectName,r=(o?n.projectsConfigurations?.projects[o]?.targets?.build:void 0)?.options?.outputPath,g=[...typeof r=="string"?[(0,node_path_1.join)(n.root,r,"main.js")]:[],(0,node_path_1.join)(e,"dist","main.js"),(0,node_path_1.join)(n.root,"dist",(0,node_path_1.relative)(n.root,e),"main.js")],p=g.find(c=>(0,node_fs_1.existsSync)(c));if(!p)return console.error(`[agent-dev] No built NestJS entry found. Looked in:
2
+ `+g.map(c=>` \u2022 ${c}`).join(`
3
3
  `)+`
4
- Build first: \`nx build ${e??"<project>"}\`.`),{success:!1};console.log(`[agent-dev] node ${l}`);const h=(0,node_child_process_1.spawn)(process.execPath,[l],{cwd:t,stdio:"inherit",env:{...process.env,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_EVENT_FILE:process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(o.root,".lensmcp","events.jsonl")}});return new Promise(c=>{const s=d(()=>{try{h.kill("SIGTERM")}catch{}},"onSignal");process.on("SIGINT",s),process.on("SIGTERM",s),h.on("exit",u=>{process.off("SIGINT",s),process.off("SIGTERM",s),c({success:u===0})})})}P(runNestjs,"runNestjs"),d(runNestjs,"runNestjs");async function runImported(t,o,e){if(!e.devCommand)return console.error('[agent-dev] kind="imported" needs a `devCommand` (your dev-server command, e.g. "npm run dev").\n Add it to the agent-dev target options, or pass `--devCommand "npm run dev"`.'),{success:!1};const r=(0,lens_frontend_1.findBridgeBundle)(o.root);if(!r)return console.error("[agent-dev] No lensmcp bridge bundle found. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/bridge.js`)."),{success:!1};const p=(0,lens_frontend_1.findMcpBundle)(o.root);p||console.warn("[agent-dev] No lensmcp-mcp bundle found \u2014 skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp`.");const l=[],h=new Map;let c=!1;function s(n,i,v,f,w=!1,a,C=!1){const S=(0,node_child_process_1.spawn)(i,v,{cwd:t,env:{...process.env,...f??{}},stdio:"inherit",shell:C});return l.push(S),S.on("exit",(E,N)=>{if(!c){if(w){const T=h.get(n)??0;if(a&&T<a.max){h.set(n,T+1),console.warn(`[agent-dev] ${n} exited (code=${E} sig=${N}); respawning in ${Math.round(a.delayMs/1e3)}s (${T+1}/${a.max}).`),setTimeout(()=>{c||s(n,i,v,f,w,a,C)},a.delayMs).unref?.();return}console.warn(`[agent-dev] ${n} exited (code=${E} sig=${N}); continuing.`);return}console.warn(`[agent-dev] ${n} exited unexpectedly (code=${E} sig=${N}); shutting down others.`),u()}}),S}P(s,"r"),d(s,"spawnChild");function u(){if(!c){c=!0;for(const n of l)try{n.kill("SIGTERM")}catch{}}}P(u,"l"),d(u,"shutdown"),process.on("SIGINT",u),process.on("SIGTERM",u);const m=process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(o.root,".lensmcp","events.jsonl");try{(0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(m),{recursive:!0});const n=(0,node_fs_1.statSync)(m,{throwIfNoEntry:!1});!n||Date.now()-n.mtimeMs>6e4?(0,node_fs_1.writeFileSync)(m,""):console.log("[agent-dev] event bridge is active (written <60s ago) \u2014 appending, not truncating.")}catch{}const g={LENSMCP_EVENT_FILE:m};console.log(`[agent-dev] event bridge \u2192 ${m}`),console.log(`[agent-dev] starting bridge \u2192 ws://127.0.0.1:${e.wsPort}`),s("bridge",process.execPath,[r],{...g,LENSMCP_WS_HOST:process.env.LENSMCP_WS_HOST??"127.0.0.1",LENSMCP_WS_PORT:String(e.wsPort)}),p&&(console.log(`[agent-dev] starting LensMCP MCP server (${p})`),s("lensmcp-mcp",process.execPath,[p],{...g,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:process.env.LENSMCP_PORT??"3000"}));const _={...g,LENSMCP_WS_PORT:String(e.wsPort)};if(e.node)if([t,o.root].some(n=>(0,node_fs_1.existsSync)((0,node_path_1.join)(n,"node_modules","@lensmcp","node-instrumentation")))){const n="--import @lensmcp/node-instrumentation/register";_.NODE_OPTIONS=process.env.NODE_OPTIONS?`${process.env.NODE_OPTIONS} ${n}`:n,console.log("[agent-dev] backend taps: NODE_OPTIONS += --import @lensmcp/node-instrumentation/register")}else console.warn("[agent-dev] @lensmcp/node-instrumentation not installed \u2014 skipping backend taps. Add it: `npm i -D @lensmcp/node-instrumentation`.");if(console.log(`[agent-dev] front-end seam: add \`import '@lensmcp/client-runtime/auto'\` to your app entry (connects to ws://127.0.0.1:${e.wsPort}).`),console.log(`[agent-dev] starting dev command: ${e.devCommand}`),s("dev",e.devCommand,[],_,!1,void 0,!0),e.chrome){const n=(0,lens_frontend_1.findCaptureRunner)(o.root),i=e.openUrl;n?i?(console.log(`[agent-dev] starting browser capture \u2192 ${i}`),s("browser-capture",process.execPath,[n],{...g,LENSMCP_DEV_URL:i,LENSMCP_TOKENS_FILE:(0,node_path_1.join)(o.root,"lensmcp.tokens.json"),LENSMCP_RULES_FILE:(0,node_path_1.join)(o.root,"lensmcp.rules.json"),LENSMCP_HEADLESS:e.headless===!1?"false":"true"},!0,{delayMs:1e4,max:5})):console.warn("[agent-dev] no `openUrl` provided \u2014 skipping Chrome capture (set openUrl to your dev URL)."):console.warn("[agent-dev] browser-capture runner not found \u2014 skipping live capture. Install the self-contained CLI: `npm i -D lensmcp`.")}return new Promise(n=>{const i=d(()=>{c?Promise.allSettled(l.map(v=>new Promise(f=>{v.exitCode!==null?f():v.on("exit",()=>f())}))).then(()=>n({success:!0})):setTimeout(i,500)},"tick");i()})}P(runImported,"runImported"),d(runImported,"runImported");function isLocalCaptureTarget(t,o=process.env){if(o.LENSMCP_CAPTURE_ALLOW_REMOTE==="1")return!0;let e;try{e=new URL(t)}catch{return!1}if(e.protocol!=="http:"&&e.protocol!=="https:")return!1;const r=e.hostname.toLowerCase();return r==="localhost"||r==="[::1]"||r==="::1"?!0:/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(r)}P(isLocalCaptureTarget,"isLocalCaptureTarget"),d(isLocalCaptureTarget,"isLocalCaptureTarget");
4
+ Build first: \`nx build ${o??"<project>"}\`.`),{success:!1};console.log(`[agent-dev] node ${p}`);const u=(0,node_child_process_1.spawn)(process.execPath,[p],{cwd:e,stdio:"inherit",env:{...process.env,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_EVENT_FILE:process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(n.root,".lensmcp","events.jsonl")}});return new Promise(c=>{const a=t(()=>{signalChild(u,"SIGTERM")},"onSignal");process.on("SIGINT",a),process.on("SIGTERM",a);const v=t(l=>{process.off("SIGINT",a),process.off("SIGTERM",a),c({success:l})},"done");onSpawnFailure(u,"nestjs",()=>v(!1)),u.on("exit",l=>v(l===0))})}d(runNestjs,"runNestjs"),t(runNestjs,"runNestjs");async function runImported(e,n,o){if(!o.devCommand)return console.error('[agent-dev] kind="imported" needs a `devCommand` (your dev-server command, e.g. "npm run dev").\n Add it to the agent-dev target options, or pass `--devCommand "npm run dev"`.'),{success:!1};const r=(0,lens_frontend_1.findBridgeBundle)(n.root);if(!r)return console.error("[agent-dev] No lensmcp bridge bundle found. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/bridge.js`)."),{success:!1};const g=(0,lens_frontend_1.findMcpBundle)(n.root);g||console.warn("[agent-dev] No lensmcp-mcp bundle found \u2014 skipping MCP server. Install the self-contained CLI: `npm i -D lensmcp`.");const p=[],u=new Map;let c=!1;function a(s,m,f,P,E=!1,M,w=!1){const _=(0,node_child_process_1.spawn)(m,f,{cwd:e,env:{...process.env,...P??{}},stdio:"inherit",shell:w});return p.push(_),onSpawnFailure(_,s,()=>{E||v()}),_.on("exit",(N,L)=>{if(!c){if(E){const $=u.get(s)??0;if(M&&$<M.max){u.set(s,$+1),console.warn(`[agent-dev] ${s} exited (code=${N} sig=${L}); respawning in ${Math.round(M.delayMs/1e3)}s (${$+1}/${M.max}).`),setTimeout(()=>{c||a(s,m,f,P,E,M,w)},M.delayMs).unref?.();return}console.warn(`[agent-dev] ${s} exited (code=${N} sig=${L}); continuing.`);return}console.warn(`[agent-dev] ${s} exited unexpectedly (code=${N} sig=${L}); shutting down others.`),v()}}),_}d(a,"a"),t(a,"spawnChild");function v(){if(!c){c=!0;for(const s of p)signalChild(s,"SIGTERM")}}d(v,"g"),t(v,"shutdown"),process.on("SIGINT",v),process.on("SIGTERM",v);const l=process.env.LENSMCP_EVENT_FILE??(0,node_path_1.join)(n.root,".lensmcp","events.jsonl");try{(0,node_fs_1.mkdirSync)((0,node_path_1.dirname)(l),{recursive:!0});const s=(0,node_fs_1.statSync)(l,{throwIfNoEntry:!1});!s||Date.now()-s.mtimeMs>6e4?(0,node_fs_1.writeFileSync)(l,""):console.log("[agent-dev] event bridge is active (written <60s ago) \u2014 appending, not truncating.")}catch{}const S={LENSMCP_EVENT_FILE:l};console.log(`[agent-dev] event bridge \u2192 ${l}`);const i=Object.values(n.projectsConfigurations?.projects??{}).map(s=>s.root),h=(0,lens_frontend_1.standaloneBridgeOrigins)(n.root,i);console.log(`[agent-dev] starting bridge \u2192 ws://127.0.0.1:${o.wsPort}`),h.hosts.length>0&&console.log(`[agent-dev] bridge admits this workspace's cluster hosts: ${h.hosts.join(", ")}`),a("bridge",process.execPath,[r],{...S,LENSMCP_WS_HOST:process.env.LENSMCP_WS_HOST??"127.0.0.1",LENSMCP_WS_PORT:String(o.wsPort),...h.allowedOrigins?{LENSMCP_BRIDGE_ALLOWED_ORIGINS:h.allowedOrigins}:{}}),g&&startMcp({mcpBundle:g,workspaceRoot:n.root,eventFile:l,mode:o.mcp,spawnChild:t((s,m,f,P)=>a(s,m,f,P),"spawnChild"),track:t(s=>{p.push(s)},"track")});const C={...S,LENSMCP_WS_PORT:String(o.wsPort)};if(o.node)if([e,n.root].some(s=>(0,node_fs_1.existsSync)((0,node_path_1.join)(s,"node_modules","@lensmcp","node-instrumentation")))){const s="--import @lensmcp/node-instrumentation/register";C.NODE_OPTIONS=process.env.NODE_OPTIONS?`${process.env.NODE_OPTIONS} ${s}`:s,console.log("[agent-dev] backend taps: NODE_OPTIONS += --import @lensmcp/node-instrumentation/register")}else console.warn("[agent-dev] @lensmcp/node-instrumentation not installed \u2014 skipping backend taps. Add it: `npm i -D @lensmcp/node-instrumentation`.");if(console.log(`[agent-dev] front-end seam: add \`import '@lensmcp/client-runtime/auto'\` to your app entry (connects to ws://127.0.0.1:${o.wsPort}).`),console.log(`[agent-dev] starting dev command: ${o.devCommand}`),a("dev",o.devCommand,[],C,!1,void 0,!0),o.chrome){const s=(0,lens_frontend_1.findCaptureRunner)(n.root),m=o.openUrl;s?m?(console.log(`[agent-dev] starting browser capture \u2192 ${m}`),a("browser-capture",process.execPath,[s],{...S,LENSMCP_DEV_URL:m,LENSMCP_TOKENS_FILE:(0,node_path_1.join)(n.root,"lensmcp.tokens.json"),LENSMCP_RULES_FILE:(0,node_path_1.join)(n.root,"lensmcp.rules.json"),LENSMCP_HEADLESS:o.headless===!1?"false":"true"},!0,{delayMs:1e4,max:5})):console.warn("[agent-dev] no `openUrl` provided \u2014 skipping Chrome capture (set openUrl to your dev URL)."):console.warn("[agent-dev] browser-capture runner not found \u2014 skipping live capture. Install the self-contained CLI: `npm i -D lensmcp`.")}return new Promise(s=>{const m=t(()=>{c?Promise.allSettled(p.map(exited)).then(()=>s({success:!0})):setTimeout(m,500)},"tick");m()})}d(runImported,"runImported"),t(runImported,"runImported");function signalChild(e,n){if(e.pid===void 0)return!1;try{return e.kill(n)}catch{return!1}}d(signalChild,"signalChild"),t(signalChild,"signalChild");function onSpawnFailure(e,n,o){e.on("error",r=>{if(e.pid!==void 0){console.warn(`[agent-dev] ${n}: ${r.message}`);return}console.error(`[agent-dev] ${n} failed to start: ${r.message}`),o()})}d(onSpawnFailure,"onSpawnFailure"),t(onSpawnFailure,"onSpawnFailure");function exited(e){return e.exitCode!==null||e.signalCode!==null||e.pid===void 0?Promise.resolve():new Promise(n=>e.once("exit",()=>n()))}d(exited,"exited"),t(exited,"exited");function resolveAgentDevMcpMode(e,n,o){for(const r of[e,n.LENSMCP_MCP_MODE,configuredMcpMode(o)])if(r==="shared"||r==="embedded")return r;return"shared"}d(resolveAgentDevMcpMode,"resolveAgentDevMcpMode"),t(resolveAgentDevMcpMode,"resolveAgentDevMcpMode");function configuredMcpMode(e){try{return JSON.parse((0,node_fs_1.readFileSync)((0,node_path_1.join)(e,".lensmcp","config.json"),"utf8"))?.mcp?.mode}catch{return}}d(configuredMcpMode,"configuredMcpMode"),t(configuredMcpMode,"configuredMcpMode");function startMcp(e){const n=e.mode==="shared"?sharedShimBeside(e.mcpBundle):void 0;if(n){e.track(ensureSharedMcpServer(n,e.workspaceRoot,e.eventFile));return}e.mode==="shared"&&console.warn(`[agent-dev] no shared-mode shim beside ${e.mcpBundle} (a lensmcp before shared mode) \u2014 starting agent-dev's own MCP server.`),console.log(`[agent-dev] starting LensMCP MCP server (${e.mcpBundle})`),console.log(mcpAuthHint()),e.spawnChild("lensmcp-mcp",process.execPath,[e.mcpBundle],{LENSMCP_EVENT_FILE:e.eventFile,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:process.env.LENSMCP_PORT??"3000"})}d(startMcp,"startMcp"),t(startMcp,"startMcp");function sharedShimBeside(e){const n=(0,node_path_1.join)((0,node_path_1.dirname)(e),"shim.js");return(0,node_fs_1.existsSync)(n)?n:void 0}d(sharedShimBeside,"sharedShimBeside"),t(sharedShimBeside,"sharedShimBeside");function ensureSharedMcpServer(e,n,o){console.log(`[agent-dev] MCP: attaching this workspace's shared server (${e})`);const r=(0,node_child_process_1.spawn)(process.execPath,[e,"--cwd",n],{cwd:n,env:{...process.env,LENSMCP_EVENT_FILE:o},stdio:["ignore","ignore","pipe"]});onSpawnFailure(r,"shared MCP shim",()=>{});let g,p=!1;return r.stderr&&(0,node_readline_1.createInterface)({input:r.stderr}).on("line",u=>{console.error(u);const c=readShimLine(u);c?.kind==="scope"&&(g=c.url),c?.kind==="attached"&&(p=!0,console.log(`[agent-dev] MCP: this workspace's shared server \u2192 ${g??"(url not reported)"} (${c.outcome})`),console.log(mcpAuthHint())),c?.kind==="unavailable"&&(p=!0,console.warn(`[agent-dev] MCP: this workspace's shared server is unavailable (${c.reason}). agent-dev starts no server of its own in shared mode; agent sessions fall back to an embedded one. For one here: LENSMCP_MCP_MODE=embedded, or the target option \`"mcp": "embedded"\`.`))}),r.on("exit",(u,c)=>{!p&&c===null&&console.warn(`[agent-dev] MCP: the shim exited (code=${u}) without reporting the shared server.`)}),r}d(ensureSharedMcpServer,"ensureSharedMcpServer"),t(ensureSharedMcpServer,"ensureSharedMcpServer");function readShimLine(e){const n=/\bshim → workspace ".*" \(.*\) → (\S+)$/.exec(e);if(n?.[1])return{kind:"scope",url:n[1]};const o=/\bshim: shared server (\S+) \(\d+ms\)$/.exec(e);if(o?.[1])return{kind:"attached",outcome:o[1]};const r=/\bshared MCP server unavailable \((.*)\) — falling back to an embedded server\.$/.exec(e);if(r?.[1]!==void 0)return{kind:"unavailable",reason:r[1]}}d(readShimLine,"readShimLine"),t(readShimLine,"readShimLine");function mcpAuthHint(e=process.env){return e.LENSMCP_ALLOW_INSECURE==="1"?"[agent-dev] MCP server: LENSMCP_ALLOW_INSECURE=1 \u2014 no credential required.":`[agent-dev] MCP server requires a bearer token (${e.LENSMCP_MCP_TOKEN?"LENSMCP_MCP_TOKEN":`${e.LENSMCP_HOME||"~"}/.lensmcp/mcp-token`}). The stdio \`lensmcp mcp\` entry sends it for you; an HTTP client entry needs "headers": { "Authorization": "Bearer \${LENSMCP_MCP_TOKEN}" }.`}d(mcpAuthHint,"mcpAuthHint"),t(mcpAuthHint,"mcpAuthHint");function isLocalCaptureTarget(e,n=process.env){if(n.LENSMCP_CAPTURE_ALLOW_REMOTE==="1")return!0;let o;try{o=new URL(e)}catch{return!1}if(o.protocol!=="http:"&&o.protocol!=="https:")return!1;const r=o.hostname.toLowerCase();return r==="localhost"||r==="[::1]"||r==="::1"?!0:/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(r)}d(isLocalCaptureTarget,"isLocalCaptureTarget"),t(isLocalCaptureTarget,"isLocalCaptureTarget");
@@ -12,4 +12,10 @@ export interface AgentDevExecutorSchema {
12
12
  wsPort?: number;
13
13
  /** For kind "imported": inject the node-instrumentation --import. Default true. */
14
14
  node?: boolean;
15
+ /**
16
+ * MCP mode: `shared` attaches the workspace's one shared server (starting it if needed); `embedded`
17
+ * runs agent-dev's own on `LENSMCP_PORT` (default 3000). Unset: `LENSMCP_MCP_MODE`, then
18
+ * `.lensmcp/config.json` `mcp.mode`, then `shared`.
19
+ */
20
+ mcp?: 'shared' | 'embedded';
15
21
  }
@@ -49,6 +49,11 @@
49
49
  "openUrl": {
50
50
  "type": "string",
51
51
  "description": "Override the URL Chrome navigates to."
52
+ },
53
+ "mcp": {
54
+ "type": "string",
55
+ "enum": ["shared", "embedded"],
56
+ "description": "MCP mode. \"shared\" attaches the workspace's one shared MCP server (on its own port from .lensmcp/config.json, started if it is not up); \"embedded\" runs agent-dev's own server on LENSMCP_PORT (default 3000). Unset: LENSMCP_MCP_MODE, then .lensmcp/config.json mcp.mode, then \"shared\"."
52
57
  }
53
58
  },
54
59
  "required": []
@@ -5,7 +5,7 @@ interface ExecutorResult {
5
5
  success: boolean;
6
6
  }
7
7
  /**
8
- * `errored` is not `failed` (issues/codex-security/53). A stage that could not RUN — the binary
8
+ * `errored` is not `failed` (issues/closed/codex-security-53). A stage that could not RUN — the binary
9
9
  * vanished, the child was killed, its output overran the capture buffer — says nothing about whether
10
10
  * the code is correct, and reporting it as a failure tells the agent its fix did not work when the
11
11
  * verification never reached a verdict.
@@ -4,7 +4,14 @@ export declare function initGenerator(tree: Tree, rawOptions?: InitGeneratorSche
4
4
  /**
5
5
  * The stdio MCP server entry a host agent uses to launch LensMCP.
6
6
  *
7
- * issues/codex-security/61: this was `npx -y lensmcp mcp` — an UNVERSIONED specifier that `-y`
7
+ * One entry serves both MCP modes, so it never names one: by default (`shared`) the process it starts
8
+ * is a thin shim into the workspace's one shared server — starting it on demand, sending this
9
+ * account's bearer only to a server that proves it holds it, and falling back to an embedded server
10
+ * when none can be used — and an `embedded` opt-out (`.lensmcp/config.json`, `LENSMCP_MCP_MODE`)
11
+ * makes the same command build its own. A `type: "http"` URL can do none of that, and without an
12
+ * `Authorization` header it is refused outright.
13
+ *
14
+ * issues/closed/codex-security-61: this was `npx -y lensmcp mcp` — an UNVERSIONED specifier that `-y`
8
15
  * installs without a word. The spec is not resolved through the workspace lockfile, so every
9
16
  * uncached launch could fetch whatever the registry currently answers with, and package lifecycle
10
17
  * and bin code then runs with the coding agent user's privileges. A compromised publisher,
@@ -26,14 +33,36 @@ export declare function initGenerator(tree: Tree, rawOptions?: InitGeneratorSche
26
33
  export declare function mcpServerEntry(workspaceRoot: string, deps?: {
27
34
  exists?: (p: string) => boolean;
28
35
  version?: string;
29
- }): {
30
- command: string;
31
- args: string[];
32
- };
36
+ }): McpServerEntry;
33
37
  /**
34
38
  * This plugin's own version, for the pin above. Walks up from the compiled module rather than
35
39
  * hard-coding a depth, because the published package root and the source tree sit at different
36
40
  * levels. Returns undefined rather than guessing — an unpinned specifier is better than a wrong one.
37
41
  */
38
42
  export declare function pluginVersion(from?: string): string | undefined;
43
+ /** A `.mcp.json` server entry as this generator writes it: a command and its arguments, nothing else. */
44
+ export interface McpServerEntry {
45
+ command: string;
46
+ args: string[];
47
+ }
48
+ /**
49
+ * Is `entry` one this generator wrote — in any release — rather than one a user customised?
50
+ *
51
+ * Exactly `{ command: 'npx', args: [<flag>, <specifier>, 'mcp'] }` with no other key, where the pair is
52
+ * one {@link mcpServerEntry} has emitted: `-y lensmcp` (before issues/closed/codex-security-61), `--yes
53
+ * lensmcp` (version unknown), `--yes lensmcp@<version>` (no local install), `--no lensmcp` (local).
54
+ * Anything else — another command, extra arguments, an `env` block — is the user's, and stays.
55
+ */
56
+ export declare function isGeneratedMcpEntry(entry: unknown): entry is McpServerEntry;
57
+ /**
58
+ * The entry to write over an existing generated one, or undefined to leave it (issues/closed/pod-memory-07).
59
+ *
60
+ * This generator never rewrote an existing entry, so a workspace set up before codex-security-61 kept
61
+ * `npx -y lensmcp mcp` — and so did a workspace that got the pinned form before it installed `lensmcp`.
62
+ * Each agent session then launches whatever `npx` resolves the bare or pinned specifier to, instead of
63
+ * the workspace's own install. Refreshed: a generated entry when the fresh one calls the local install
64
+ * (`--no`), and the unpinned legacy forms always. A pinned entry with still no local install is a pin,
65
+ * and is left as it is; so is anything a user customised.
66
+ */
67
+ export declare function refreshedMcpEntry(existing: unknown, fresh: McpServerEntry): McpServerEntry | undefined;
39
68
  export default initGenerator;
@@ -1,9 +1,10 @@
1
- "use strict";var f=Object.defineProperty;var p=(s,r)=>f(s,"name",{value:r,configurable:!0});var d=Object.defineProperty,c=p((s,r)=>d(s,"name",{value:r,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.initGenerator=initGenerator,exports.mcpServerEntry=mcpServerEntry,exports.pluginVersion=pluginVersion;const node_fs_1=require("node:fs"),node_path_1=require("node:path"),devkit_1=require("@nx/devkit"),DEFAULT_LENSMCP_CONFIG={schemaVersion:1,storage:"memory",transport:"stdio",retention:{sessions:5,events:5e3,screenshots:50,buildReports:20,visualFrames:200,heapSnapshots:3,tracesWithErrors:20,verifications:10},redaction:{headers:["authorization","cookie","set-cookie","x-api-key"],paths:["password","token","secret","apiKey","api_key"]},channels:{blockingStatusChanged:{enabled:!0,minSeverity:"error"},memoryLeakSuspected:{enabled:!0,minSeverity:"warning"},bundleRegression:{enabled:!0,minSeverity:"warning"},visualViolationBlocking:{enabled:!0,minSeverity:"error"},verificationCompleted:{enabled:!0,minSeverity:"info"}},thresholds:{dbInLoop:5,nPlusOne:3,renderStorm:5,slowRouteMs:500,slowRenderMs:16}},LENSMCP_GITIGNORE_MARKER="# LensMCP runtime artifacts (per-session storage)",LENSMCP_GITIGNORE_ENTRY=".lensmcp/";async function initGenerator(s,r={}){const n={skipFormat:r.skipFormat??!1,registerHostConfig:r.registerHostConfig??"auto"},e=[];if(s.exists("nx.json"))(0,devkit_1.updateJson)(s,"nx.json",i=>{i.plugins=Array.isArray(i.plugins)?i.plugins:[],i.plugins.some(a=>typeof a=="string"?a==="@lensmcp/nx-plugin":a&&typeof a=="object"&&"plugin"in a?a.plugin==="@lensmcp/nx-plugin":!1)||(i.plugins.push("@lensmcp/nx-plugin"),e.push("nx.json: registered @lensmcp/nx-plugin"));const g=i.lensmcp??{};return i.lensmcp=mergeDeep(DEFAULT_LENSMCP_CONFIG,g),(!("lensmcp"in i)||Object.keys(g).length===0)&&e.push("nx.json: added workspace-wide `lensmcp` config block"),i});else throw new Error("No nx.json found at the workspace root. Are you in a Nx workspace?");const t=".gitignore",o=s.exists(t)?s.read(t,"utf-8")??"":"";if(!o.split(`
2
- `).some(i=>i.trim()===LENSMCP_GITIGNORE_ENTRY)){const i=o.endsWith(`
1
+ "use strict";var f=Object.defineProperty;var p=(e,s)=>f(e,"name",{value:s,configurable:!0});var d=Object.defineProperty,c=p((e,s)=>d(e,"name",{value:s,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.initGenerator=initGenerator,exports.mcpServerEntry=mcpServerEntry,exports.pluginVersion=pluginVersion,exports.isGeneratedMcpEntry=isGeneratedMcpEntry,exports.refreshedMcpEntry=refreshedMcpEntry;const node_fs_1=require("node:fs"),node_path_1=require("node:path"),devkit_1=require("@nx/devkit"),DEFAULT_LENSMCP_CONFIG={schemaVersion:1,storage:"memory",transport:"stdio",retention:{sessions:5,events:5e3,screenshots:50,buildReports:20,visualFrames:200,heapSnapshots:3,tracesWithErrors:20,verifications:10},redaction:{headers:["authorization","cookie","set-cookie","x-api-key"],paths:["password","token","secret","apiKey","api_key"]},channels:{blockingStatusChanged:{enabled:!0,minSeverity:"error"},memoryLeakSuspected:{enabled:!0,minSeverity:"warning"},bundleRegression:{enabled:!0,minSeverity:"warning"},visualViolationBlocking:{enabled:!0,minSeverity:"error"},verificationCompleted:{enabled:!0,minSeverity:"info"}},thresholds:{dbInLoop:5,nPlusOne:3,renderStorm:5,slowRouteMs:500,slowRenderMs:16}},LENSMCP_GITIGNORE_MARKER="# LensMCP runtime artifacts (per-session storage)",LENSMCP_GITIGNORE_ENTRY=".lensmcp/";async function initGenerator(e,s={}){const r={skipFormat:s.skipFormat??!1,registerHostConfig:s.registerHostConfig??"auto"},n=[];if(e.exists("nx.json"))(0,devkit_1.updateJson)(e,"nx.json",o=>{o.plugins=Array.isArray(o.plugins)?o.plugins:[],o.plugins.some(l=>typeof l=="string"?l==="@lensmcp/nx-plugin":l&&typeof l=="object"&&"plugin"in l?l.plugin==="@lensmcp/nx-plugin":!1)||(o.plugins.push("@lensmcp/nx-plugin"),n.push("nx.json: registered @lensmcp/nx-plugin"));const g=o.lensmcp??{};return o.lensmcp=mergeDeep(DEFAULT_LENSMCP_CONFIG,g),(!("lensmcp"in o)||Object.keys(g).length===0)&&n.push("nx.json: added workspace-wide `lensmcp` config block"),o});else throw new Error("No nx.json found at the workspace root. Are you in a Nx workspace?");const t=".gitignore",i=e.exists(t)?e.read(t,"utf-8")??"":"";if(!i.split(`
2
+ `).some(o=>o.trim()===LENSMCP_GITIGNORE_ENTRY)){const o=i.endsWith(`
3
3
  `)?"":`
4
- `;s.write(t,`${o}${i}
4
+ `;e.write(t,`${i}${o}
5
5
  ${LENSMCP_GITIGNORE_MARKER}
6
6
  ${LENSMCP_GITIGNORE_ENTRY}
7
- `),e.push(`${t}: added \`${LENSMCP_GITIGNORE_ENTRY}\``)}const l=(0,devkit_1.joinPathFragments)(".lensmcp",".keep");s.exists(l)||(s.write(l,""),e.push(`${l}: created`));const m=(0,devkit_1.joinPathFragments)(".lensmcp","install-trail.json"),u=(s.exists(m)?JSON.parse(s.read(m,"utf-8")??"{}"):void 0)??{schemaVersion:1,installedAt:new Date().toISOString(),steps:[]};u.steps.push(...e),s.write(m,JSON.stringify(u,null,2)+`
8
- `),n.registerHostConfig!=="never"&&registerWorkspaceMcpConfig(s,e),n.skipFormat||await(0,devkit_1.formatFiles)(s)}p(initGenerator,"initGenerator"),c(initGenerator,"initGenerator");function mcpServerEntry(s,r={}){if((r.exists??node_fs_1.existsSync)((0,node_path_1.join)(s,"node_modules",".bin","lensmcp")))return{command:"npx",args:["--no","lensmcp","mcp"]};const n="version"in r?r.version:pluginVersion();return{command:"npx",args:["--yes",n?`lensmcp@${n}`:"lensmcp","mcp"]}}p(mcpServerEntry,"mcpServerEntry"),c(mcpServerEntry,"mcpServerEntry");function pluginVersion(s=__dirname){let r=s;for(let n=0;n<6;n++){const e=(0,node_path_1.join)(r,"package.json");try{if((0,node_fs_1.existsSync)(e)){const o=JSON.parse((0,node_fs_1.readFileSync)(e,"utf-8"));if(o.name==="@lensmcp/nx-plugin"&&typeof o.version=="string")return o.version}}catch{}const t=(0,node_path_1.dirname)(r);if(t===r)break;r=t}}p(pluginVersion,"pluginVersion"),c(pluginVersion,"pluginVersion");function registerWorkspaceMcpConfig(s,r){const n=".mcp.json";let e={};if(s.exists(n))try{e=JSON.parse(s.read(n,"utf-8")??"{}")}catch{e={}}if((!e||typeof e!="object")&&(e={}),(!e.mcpServers||typeof e.mcpServers!="object")&&(e.mcpServers={}),e.mcpServers.lensmcp)return;const t=mcpServerEntry(s.root);e.mcpServers.lensmcp=t,s.write(n,JSON.stringify(e,null,2)+`
9
- `),r.push(`.mcp.json: registered lensmcp MCP server (stdio, ${t.args.join(" ")})`)}p(registerWorkspaceMcpConfig,"registerWorkspaceMcpConfig"),c(registerWorkspaceMcpConfig,"registerWorkspaceMcpConfig"),exports.default=initGenerator;function mergeDeep(s,r){const n={...s};for(const[e,t]of Object.entries(r))t!==null&&typeof t=="object"&&!Array.isArray(t)&&n[e]!==null&&typeof n[e]=="object"&&!Array.isArray(n[e])?n[e]=mergeDeep(n[e],t):n[e]=t;return n}p(mergeDeep,"mergeDeep"),c(mergeDeep,"mergeDeep");
7
+ `),n.push(`${t}: added \`${LENSMCP_GITIGNORE_ENTRY}\``)}const a=(0,devkit_1.joinPathFragments)(".lensmcp",".keep");e.exists(a)||(e.write(a,""),n.push(`${a}: created`));const m=(0,devkit_1.joinPathFragments)(".lensmcp","install-trail.json"),u=(e.exists(m)?JSON.parse(e.read(m,"utf-8")??"{}"):void 0)??{schemaVersion:1,installedAt:new Date().toISOString(),steps:[]};u.steps.push(...n),e.write(m,JSON.stringify(u,null,2)+`
8
+ `),r.registerHostConfig!=="never"&&registerWorkspaceMcpConfig(e,n),r.skipFormat||await(0,devkit_1.formatFiles)(e)}p(initGenerator,"initGenerator"),c(initGenerator,"initGenerator");function mcpServerEntry(e,s={}){if((s.exists??node_fs_1.existsSync)((0,node_path_1.join)(e,"node_modules",".bin","lensmcp")))return{command:"npx",args:["--no","lensmcp","mcp"]};const r="version"in s?s.version:pluginVersion();return{command:"npx",args:["--yes",r?`lensmcp@${r}`:"lensmcp","mcp"]}}p(mcpServerEntry,"mcpServerEntry"),c(mcpServerEntry,"mcpServerEntry");function pluginVersion(e=__dirname){let s=e;for(let r=0;r<6;r++){const n=(0,node_path_1.join)(s,"package.json");try{if((0,node_fs_1.existsSync)(n)){const i=JSON.parse((0,node_fs_1.readFileSync)(n,"utf-8"));if(i.name==="@lensmcp/nx-plugin"&&typeof i.version=="string")return i.version}}catch{}const t=(0,node_path_1.dirname)(s);if(t===s)break;s=t}}p(pluginVersion,"pluginVersion"),c(pluginVersion,"pluginVersion");const PINNED_SPECIFIER=/^lensmcp@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;function isGeneratedMcpEntry(e){if(!e||typeof e!="object"||Array.isArray(e))return!1;const{command:s,args:r,...n}=e;if(s!=="npx"||Object.keys(n).length>0||!Array.isArray(r)||r.length!==3)return!1;const[t,i,a]=r;return a!=="mcp"||typeof i!="string"?!1:t==="--no"?i==="lensmcp":t==="-y"||t==="--yes"?i==="lensmcp"||PINNED_SPECIFIER.test(i):!1}p(isGeneratedMcpEntry,"isGeneratedMcpEntry"),c(isGeneratedMcpEntry,"isGeneratedMcpEntry");function refreshedMcpEntry(e,s){if(!isGeneratedMcpEntry(e)||e.args.join(" ")===s.args.join(" "))return;const r=e.args[0]!=="--no"&&e.args[1]==="lensmcp";return s.args[0]==="--no"||r?s:void 0}p(refreshedMcpEntry,"refreshedMcpEntry"),c(refreshedMcpEntry,"refreshedMcpEntry");function registerWorkspaceMcpConfig(e,s){const r=".mcp.json";let n={};if(e.exists(r))try{n=JSON.parse(e.read(r,"utf-8")??"{}")}catch{n={}}(!n||typeof n!="object")&&(n={}),(!n.mcpServers||typeof n.mcpServers!="object")&&(n.mcpServers={});const t=n.mcpServers.lensmcp,i=mcpServerEntry(e.root);if(t){const a=refreshedMcpEntry(t,i);if(!a)return;n.mcpServers.lensmcp=a,e.write(r,JSON.stringify(n,null,2)+`
9
+ `),s.push(`.mcp.json: refreshed the generated lensmcp MCP entry (${t.args.join(" ")} \u2192 ${a.args.join(" ")})`);return}n.mcpServers.lensmcp=i,e.write(r,JSON.stringify(n,null,2)+`
10
+ `),s.push(`.mcp.json: registered lensmcp MCP server (stdio, ${i.args.join(" ")})`)}p(registerWorkspaceMcpConfig,"registerWorkspaceMcpConfig"),c(registerWorkspaceMcpConfig,"registerWorkspaceMcpConfig"),exports.default=initGenerator;function mergeDeep(e,s){const r={...e};for(const[n,t]of Object.entries(s))t!==null&&typeof t=="object"&&!Array.isArray(t)&&r[n]!==null&&typeof r[n]=="object"&&!Array.isArray(r[n])?r[n]=mergeDeep(r[n],t):r[n]=t;return r}p(mergeDeep,"mergeDeep"),c(mergeDeep,"mergeDeep");
@@ -73,3 +73,36 @@ export declare function findCaptureRunner(workspaceRoot: string): string | undef
73
73
  * `lensmcp` package's `bundled/bridge.js`, else the in-repo dev build.
74
74
  */
75
75
  export declare function findBridgeBundle(workspaceRoot: string): string | undefined;
76
+ /** The part of a project's `cluster` (legacy `davnx`) declaration the bridge allowlist reads. */
77
+ export interface ClusterPageDecl {
78
+ /** The public dev hostname the gateway serves the project under (`app.shop.local`, `*.shop.local`). */
79
+ host?: string;
80
+ /** Set for a pod service — a backend, never a page the client runtime runs in. */
81
+ service?: string;
82
+ }
83
+ /** The origins to hand a standalone bridge: this workspace's page hosts, merged into any the caller set. */
84
+ export interface StandaloneBridgeOrigins {
85
+ /** The cluster's own page hosts, as allowlist entries. */
86
+ hosts: string[];
87
+ /** The `LENSMCP_BRIDGE_ALLOWED_ORIGINS` value to start the bridge with; absent when there is nothing to admit. */
88
+ allowedOrigins?: string;
89
+ }
90
+ /**
91
+ * The page hosts a workspace's own cluster serves — every frontend `host` the gateway routes — as
92
+ * `LENSMCP_BRIDGE_ALLOWED_ORIGINS` entries.
93
+ *
94
+ * The standalone bridge admits loopback page origins only (issues/closed/codex-security-32, 41, 68), and
95
+ * a webpack / Next.js / no-build app behind the gateway runs at `https://<host>`, so its page was
96
+ * refused at the upgrade unless someone set the variable by hand. The Vite plugin learns the origins it
97
+ * serves its runtime to; a standalone bridge serves nothing, so whoever STARTS one passes these. Pod
98
+ * services are skipped — they are backends, not pages — and a wildcard host (`*.shop.local`) becomes
99
+ * the subdomain form (`.shop.local`). Anything else (a LAN address, a tenant subdomain the default
100
+ * route catches) still goes in `LENSMCP_BRIDGE_ALLOWED_ORIGINS`.
101
+ */
102
+ export declare function clusterPageHosts(decls: readonly ClusterPageDecl[]): string[];
103
+ /** `existing` (a caller's own allowlist) plus `hosts`, comma-joined and de-duplicated; undefined when both are empty. */
104
+ export declare function withBridgeAllowedOrigins(existing: string | undefined, hosts: readonly string[]): string | undefined;
105
+ /** The `cluster` (or legacy `davnx`) declarations in each project's `project.json`; unreadable ones are skipped. */
106
+ export declare function readClusterPageDecls(workspaceRoot: string, projectRoots: readonly string[]): ClusterPageDecl[];
107
+ /** What a standalone bridge started for this workspace should admit, given the caller's environment. */
108
+ export declare function standaloneBridgeOrigins(workspaceRoot: string, projectRoots: readonly string[], env?: NodeJS.ProcessEnv): StandaloneBridgeOrigins;
package/lens-frontend.js CHANGED
@@ -1 +1 @@
1
- "use strict";var _=Object.defineProperty;var s=(n,e)=>_(n,"name",{value:e,configurable:!0});var g=Object.defineProperty,t=s((n,e)=>g(n,"name",{value:e,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.spawnLensFrontend=spawnLensFrontend,exports.locateBin=locateBin,exports.resolveFirstExisting=resolveFirstExisting,exports.globalNodeModules=globalNodeModules,exports.findMcpBundle=findMcpBundle,exports.findDashboardBundle=findDashboardBundle,exports.findCaptureRunner=findCaptureRunner,exports.findBridgeBundle=findBridgeBundle;const node_child_process_1=require("node:child_process"),node_fs_1=require("node:fs"),node_path_1=require("node:path");function spawnLensFrontend(n,e){const i=n.log??(l=>console.log(l)),{projectRoot:o,workspaceRoot:c,eventFile:v}=n,u=n.chrome!==!1,b=n.headless!==!1,p=n.viteBin??locateBin("vite",[o,c]);if(!p)throw new Error("[lens] Could not find the vite binary in node_modules/.bin.");const r=n.port??(u?5173:void 0),a=["--config",resolveFirstExisting(["vite.config.ts","vite.config.mts","vite.config.js","vite.config.mjs","vite.config.cts","vite.config.cjs"],o)??(0,node_path_1.join)(o,"vite.config.ts"),o];r!==void 0&&a.push("--port",String(r),"--strictPort");const f={LENSMCP_EVENT_FILE:v,...n.viteEnv??{}},j=n.labelPrefix??"";i(`[lens] starting Vite for ${n.project??o}${r?` on :${r}`:""}`);const m=e(`${j}vite`,p,a,f),d=n.openUrl??(r?`http://localhost:${r}/`:void 0);if(u){const l=findCaptureRunner(c);l?d?(i(`[lens] starting browser capture \u2192 ${d}`),e(`${j}browser-capture`,process.execPath,[l],{...f,LENSMCP_DEV_URL:d,LENSMCP_TOKENS_FILE:(0,node_path_1.join)(c,"lensmcp.tokens.json"),LENSMCP_RULES_FILE:(0,node_path_1.join)(c,"lensmcp.rules.json"),LENSMCP_HEADLESS:b===!1?"false":"true"},!0,{delayMs:1e4,max:5})):i("[lens] no dev URL/port resolved \u2014 skipping live capture."):i("[lens] browser-capture runner not found \u2014 skipping live capture. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/capture-runner.js`).")}return{...r!==void 0?{devPort:r}:{},...d?{devUrl:d}:{},vite:m}}s(spawnLensFrontend,"spawnLensFrontend"),t(spawnLensFrontend,"spawnLensFrontend");function locateBin(n,e){for(const i of e){const o=(0,node_path_1.join)(i,"node_modules",".bin",n);if((0,node_fs_1.existsSync)(o))return o}}s(locateBin,"locateBin"),t(locateBin,"locateBin");function resolveFirstExisting(n,e){for(const i of n){const o=(0,node_path_1.join)(e,i);if((0,node_fs_1.existsSync)(o))return o}}s(resolveFirstExisting,"resolveFirstExisting"),t(resolveFirstExisting,"resolveFirstExisting");let cachedGlobalRoot;function globalNodeModules(){if(cachedGlobalRoot!==void 0)return cachedGlobalRoot??void 0;const n=(0,node_path_1.resolve)((0,node_path_1.dirname)(process.execPath),"..","lib","node_modules");if((0,node_fs_1.existsSync)(n))return cachedGlobalRoot=n,n;try{const e=(0,node_child_process_1.execSync)("npm root -g",{encoding:"utf8",timeout:5e3}).trim();cachedGlobalRoot=e&&(0,node_fs_1.existsSync)(e)?e:null}catch{cachedGlobalRoot=null}return cachedGlobalRoot??void 0}s(globalNodeModules,"globalNodeModules"),t(globalNodeModules,"globalNodeModules");function findMcpBundle(n){const e=globalNodeModules();return firstExisting([(0,node_path_1.join)(n,"node_modules","lensmcp","bundled","main.js"),...e?[(0,node_path_1.join)(e,"lensmcp","bundled","main.js")]:[],(0,node_path_1.join)(n,"servers","lensmcp-mcp","dist","main.js")])}s(findMcpBundle,"findMcpBundle"),t(findMcpBundle,"findMcpBundle");function findDashboardBundle(n){const e=globalNodeModules();return firstExisting([(0,node_path_1.join)(n,"node_modules","lensmcp","bundled","dashboard.js"),...e?[(0,node_path_1.join)(e,"lensmcp","bundled","dashboard.js")]:[],(0,node_path_1.join)(n,"servers","lensmcp-mcp","dist","dashboard.js")])}s(findDashboardBundle,"findDashboardBundle"),t(findDashboardBundle,"findDashboardBundle");function findCaptureRunner(n){const e=globalNodeModules();return firstExisting([(0,node_path_1.join)(n,"node_modules","lensmcp","bundled","capture-runner.js"),...e?[(0,node_path_1.join)(e,"lensmcp","bundled","capture-runner.js")]:[],(0,node_path_1.join)(n,"libs","browser-capture","dist","capture-runner.js")])}s(findCaptureRunner,"findCaptureRunner"),t(findCaptureRunner,"findCaptureRunner");function findBridgeBundle(n){const e=globalNodeModules();return firstExisting([(0,node_path_1.join)(n,"node_modules","lensmcp","bundled","bridge.js"),...e?[(0,node_path_1.join)(e,"lensmcp","bundled","bridge.js")]:[],(0,node_path_1.join)(n,"libs","bridge","dist","main.js")])}s(findBridgeBundle,"findBridgeBundle"),t(findBridgeBundle,"findBridgeBundle");function firstExisting(n){for(const e of n)if((0,node_fs_1.existsSync)(e))return e}s(firstExisting,"firstExisting"),t(firstExisting,"firstExisting");
1
+ "use strict";var h=Object.defineProperty;var r=(e,n)=>h(e,"name",{value:n,configurable:!0});var m=Object.defineProperty,o=r((e,n)=>m(e,"name",{value:n,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.spawnLensFrontend=spawnLensFrontend,exports.locateBin=locateBin,exports.resolveFirstExisting=resolveFirstExisting,exports.globalNodeModules=globalNodeModules,exports.findMcpBundle=findMcpBundle,exports.findDashboardBundle=findDashboardBundle,exports.findCaptureRunner=findCaptureRunner,exports.findBridgeBundle=findBridgeBundle,exports.clusterPageHosts=clusterPageHosts,exports.withBridgeAllowedOrigins=withBridgeAllowedOrigins,exports.readClusterPageDecls=readClusterPageDecls,exports.standaloneBridgeOrigins=standaloneBridgeOrigins;const node_child_process_1=require("node:child_process"),node_fs_1=require("node:fs"),node_path_1=require("node:path");function spawnLensFrontend(e,n){const t=e.log??(u=>console.log(u)),{projectRoot:s,workspaceRoot:i,eventFile:c}=e,a=e.chrome!==!1,v=e.headless!==!1,p=e.viteBin??locateBin("vite",[s,i]);if(!p)throw new Error("[lens] Could not find the vite binary in node_modules/.bin.");const d=e.port??(a?5173:void 0),f=["--config",resolveFirstExisting(["vite.config.ts","vite.config.mts","vite.config.js","vite.config.mjs","vite.config.cts","vite.config.cjs"],s)??(0,node_path_1.join)(s,"vite.config.ts"),s];d!==void 0&&f.push("--port",String(d),"--strictPort");const g={LENSMCP_EVENT_FILE:c,...e.viteEnv??{}},j=e.labelPrefix??"";t(`[lens] starting Vite for ${e.project??s}${d?` on :${d}`:""}`);const b=n(`${j}vite`,p,f,g),l=e.openUrl??(d?`http://localhost:${d}/`:void 0);if(a){const u=findCaptureRunner(i);u?l?(t(`[lens] starting browser capture \u2192 ${l}`),n(`${j}browser-capture`,process.execPath,[u],{...g,LENSMCP_DEV_URL:l,LENSMCP_TOKENS_FILE:(0,node_path_1.join)(i,"lensmcp.tokens.json"),LENSMCP_RULES_FILE:(0,node_path_1.join)(i,"lensmcp.rules.json"),LENSMCP_HEADLESS:v===!1?"false":"true"},!0,{delayMs:1e4,max:5})):t("[lens] no dev URL/port resolved \u2014 skipping live capture."):t("[lens] browser-capture runner not found \u2014 skipping live capture. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/capture-runner.js`).")}return{...d!==void 0?{devPort:d}:{},...l?{devUrl:l}:{},vite:b}}r(spawnLensFrontend,"spawnLensFrontend"),o(spawnLensFrontend,"spawnLensFrontend");function locateBin(e,n){for(const t of n){const s=(0,node_path_1.join)(t,"node_modules",".bin",e);if((0,node_fs_1.existsSync)(s))return s}}r(locateBin,"locateBin"),o(locateBin,"locateBin");function resolveFirstExisting(e,n){for(const t of e){const s=(0,node_path_1.join)(n,t);if((0,node_fs_1.existsSync)(s))return s}}r(resolveFirstExisting,"resolveFirstExisting"),o(resolveFirstExisting,"resolveFirstExisting");let cachedGlobalRoot;function globalNodeModules(){if(cachedGlobalRoot!==void 0)return cachedGlobalRoot??void 0;const e=(0,node_path_1.resolve)((0,node_path_1.dirname)(process.execPath),"..","lib","node_modules");if((0,node_fs_1.existsSync)(e))return cachedGlobalRoot=e,e;try{const n=(0,node_child_process_1.execSync)("npm root -g",{encoding:"utf8",timeout:5e3}).trim();cachedGlobalRoot=n&&(0,node_fs_1.existsSync)(n)?n:null}catch{cachedGlobalRoot=null}return cachedGlobalRoot??void 0}r(globalNodeModules,"globalNodeModules"),o(globalNodeModules,"globalNodeModules");function findMcpBundle(e){const n=globalNodeModules();return firstExisting([(0,node_path_1.join)(e,"node_modules","lensmcp","bundled","main.js"),...n?[(0,node_path_1.join)(n,"lensmcp","bundled","main.js")]:[],(0,node_path_1.join)(e,"servers","lensmcp-mcp","dist","main.js")])}r(findMcpBundle,"findMcpBundle"),o(findMcpBundle,"findMcpBundle");function findDashboardBundle(e){const n=globalNodeModules();return firstExisting([(0,node_path_1.join)(e,"node_modules","lensmcp","bundled","dashboard.js"),...n?[(0,node_path_1.join)(n,"lensmcp","bundled","dashboard.js")]:[],(0,node_path_1.join)(e,"servers","lensmcp-mcp","dist","dashboard.js")])}r(findDashboardBundle,"findDashboardBundle"),o(findDashboardBundle,"findDashboardBundle");function findCaptureRunner(e){const n=globalNodeModules();return firstExisting([(0,node_path_1.join)(e,"node_modules","lensmcp","bundled","capture-runner.js"),...n?[(0,node_path_1.join)(n,"lensmcp","bundled","capture-runner.js")]:[],(0,node_path_1.join)(e,"libs","browser-capture","dist","capture-runner.js")])}r(findCaptureRunner,"findCaptureRunner"),o(findCaptureRunner,"findCaptureRunner");function findBridgeBundle(e){const n=globalNodeModules();return firstExisting([(0,node_path_1.join)(e,"node_modules","lensmcp","bundled","bridge.js"),...n?[(0,node_path_1.join)(n,"lensmcp","bundled","bridge.js")]:[],(0,node_path_1.join)(e,"libs","bridge","dist","main.js")])}r(findBridgeBundle,"findBridgeBundle"),o(findBridgeBundle,"findBridgeBundle");function firstExisting(e){for(const n of e)if((0,node_fs_1.existsSync)(n))return n}r(firstExisting,"firstExisting"),o(firstExisting,"firstExisting");function clusterPageHosts(e){const n=new Set;for(const t of e){if(t.service||typeof t.host!="string")continue;const s=t.host.trim().toLowerCase();s&&n.add(s.startsWith("*.")?s.slice(1):s)}return[...n]}r(clusterPageHosts,"clusterPageHosts"),o(clusterPageHosts,"clusterPageHosts");function withBridgeAllowedOrigins(e,n){const t=[...new Set([...(e??"").split(/[\s,]+/).filter(Boolean),...n])];return t.length>0?t.join(","):void 0}r(withBridgeAllowedOrigins,"withBridgeAllowedOrigins"),o(withBridgeAllowedOrigins,"withBridgeAllowedOrigins");function readClusterPageDecls(e,n){const t=[];for(const s of n)try{const i=JSON.parse((0,node_fs_1.readFileSync)((0,node_path_1.join)(e,s,"project.json"),"utf8")),c=i.cluster??i.davnx;c&&typeof c=="object"&&t.push(c)}catch{}return t}r(readClusterPageDecls,"readClusterPageDecls"),o(readClusterPageDecls,"readClusterPageDecls");function standaloneBridgeOrigins(e,n,t=process.env){const s=clusterPageHosts(readClusterPageDecls(e,n)),i=withBridgeAllowedOrigins(t.LENSMCP_BRIDGE_ALLOWED_ORIGINS,s);return i?{hosts:s,allowedOrigins:i}:{hosts:s}}r(standaloneBridgeOrigins,"standaloneBridgeOrigins"),o(standaloneBridgeOrigins,"standaloneBridgeOrigins");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/nx-plugin",
3
- "version": "1.23.13",
3
+ "version": "1.24.0",
4
4
  "main": "./index.js",
5
5
  "module": "./index.js",
6
6
  "types": "./index.d.ts",