@agimon-ai/browse-tool 0.2.13 → 0.2.14
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/dist/cli.cjs +16 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +23 -23
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{playwright-test-DfpyCIc0.cjs → playwright-test-jbz0wL4w.cjs} +2 -2
- package/dist/{playwright-test-DfpyCIc0.cjs.map → playwright-test-jbz0wL4w.cjs.map} +1 -1
- package/dist/{streamable-http-O6YYT3Yr.cjs → streamable-http-BAKht810.cjs} +8 -8
- package/dist/{streamable-http-O6YYT3Yr.cjs.map → streamable-http-BAKht810.cjs.map} +1 -1
- package/dist/streamable-http-Beq9pCY3.mjs +15 -0
- package/dist/{streamable-http-_UIJOzAB.mjs.map → streamable-http-Beq9pCY3.mjs.map} +1 -1
- package/dist/stubs/playwright-test.cjs +1 -1
- package/package.json +4 -4
- package/dist/streamable-http-_UIJOzAB.mjs +0 -15
package/dist/cli.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const e=require(`./streamable-http-O6YYT3Yr.cjs`);require(`./playwright-test-DfpyCIc0.cjs`),require(`reflect-metadata/lite`);let t=require(`commander`),n=require(`@hono/node-server`),r=require(`@modelcontextprotocol/sdk/server/index.js`),i=require(`@modelcontextprotocol/sdk/server/stdio.js`),a=require(`@modelcontextprotocol/sdk/types.js`),o=require(`hono`),s=require(`hono/cors`),c=require(`inversify`),l=require(`node:fs`),u=require(`node:path`);u=e.w(u);let d=require(`@opentelemetry/api`),f=require(`@agimon-ai/foundation-port-registry`),p=require(`node:os`),m=require(`zod`),h=require(`node:fs/promises`),g=require(`node:module`),_=require(`@hono/node-ws`),v=require(`hono/html`),y=require(`hono/jsx/jsx-runtime`);var b=`0.2.12`;const x=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function S(e,t){if(!x)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function C(t){let n=new o.Hono,r;try{r=t.get(e.C.TelemetryService)}catch{r=new e.x}return n.get(`/telemetry-config`,t=>{try{let n=new URL(t.req.url).origin;return t.json(e.S(process.env,n))}catch(e){return t.json({enabled:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tasks`,n=>{try{let r=t.get(e.C.ExtensionTaskQueue).getNextTask();return r?n.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):n.json({})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/result`,async n=>{try{let r=await n.req.json();if(!r.taskId)return n.json({success:!1,error:`Missing taskId in request body`},400);let i=t.get(e.C.ExtensionTaskQueue),a=t.get(e.C.BrowserService),o={taskId:r.taskId,success:r.success,result:r.result,error:r.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),n.json({success:!0})):n.json({success:!1,error:`Task ${r.taskId} not found or already completed`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/status`,n=>{try{let r=t.get(e.C.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.queueSize};return n.json(a)}catch(e){return n.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/register`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).register(r);return n.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/heartbeat`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).heartbeat(r);return i?n.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/tab-mapped`,async n=>{try{let r=await n.req.json();if(!r.pageId||typeof r.tabId!=`number`)return n.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=t.get(e.C.PageRegistry),a=t.get(e.C.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),n.json({success:!0})):n.json({success:!1,error:`Page ${r.pageId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.C.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return S(`artifact received`,{browserId:r.browserId,videoBase64Size:r.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(r.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${r.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording/chunk`,async n=>{try{let i=await n.req.json();if(!i.browserId||!i.chunkBase64)return n.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=t.get(e.C.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return S(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(r.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":i.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof i.chunkIndex==`number`?{"browse_tool.recording.chunk_index":i.chunkIndex}:{},...typeof i.mimeType==`string`?{"browse_tool.recording.mime_type":i.mimeType}:{}}}),a.recordBrowserActivity(i.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/browser-log`,async e=>{try{let t=await e.req.json();return!t.message||typeof t.message!=`string`?e.json({success:!1,error:`Missing message in request body`},400):(r.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),S(`browser log relayed`,{level:t.level??`info`,message:t.message,attributes:t.attributes}),e.json({success:!0}))}catch(t){return e.json({success:!1,error:t instanceof Error?t.message:String(t)},500)}}),n.post(`/handoff`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).requestHandoff(r);return i?n.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/handoff/acknowledge`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).acknowledgeHandoff(r.sessionId);return i?n.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found or no handoff pending`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/sessions`,n=>{try{let r=t.get(e.C.ExtensionSessionRegistry).listSessions();return n.json({sessions:r.map(e=>({id:e.id,browserId:e.browserId,tabId:e.tabId,currentUrl:e.currentUrl,controlMode:e.controlMode,activeSpecPath:e.activeSpecPath,handoffRequested:e.handoffRequested,createdAt:e.createdAt.toISOString(),lastHeartbeatAt:e.lastHeartbeatAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n}function w(){return new c.ContainerModule(t=>{t.bind(e.C.ExtensionTaskQueue).to(e.b).inSingletonScope(),t.bind(e.C.ExtensionToolDelegator).to(e.y).inSingletonScope()})}function T(e){let t=new r.Server({name:`browse-tool-chrome`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getSupportedTools().map(e=>({name:e,description:`Browser automation tool (Chrome extension mode): ${e}`,inputSchema:{type:`object`,properties:{},additionalProperties:!0}}));return t.setRequestHandler(a.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(a.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const E=new t.Command(`chrome-serve`).description(`[DEPRECATED] Start MCP server with Chrome extension HTTP polling for bot-detection-free browser automation`).option(`-p, --port <port>`,`HTTP server port for extension polling`,`3200`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async t=>{console.error(``),console.error(`╔════════════════════════════════════════════════════════════════╗`),console.error(`║ DEPRECATED: chrome-serve is deprecated. ║`),console.error(`║ Use mcp-serve instead - extension routes are now ║`),console.error(`║ automatically available in the HTTP server at /extension/*. ║`),console.error(`╚════════════════════════════════════════════════════════════════╝`),console.error(``);try{let r=typeof t.port==`string`?Number.parseInt(t.port,10):t.port;t.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${r}`),console.error(` Wait for extension: ${t.waitForExtension}`));let a=new c.Container({defaultScope:`Singleton`});a.load(w());let l=a.get(e.C.ExtensionTaskQueue),u=a.get(e.C.ExtensionToolDelegator),d=new o.Hono;d.use(`*`,(0,s.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=C(a);d.route(`/extension`,f),d.get(`/health`,e=>{let t=l.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let p=(0,n.serve)({fetch:d.fetch,port:r});p.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${r} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${r}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${r}/extension/tasks`),t.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{l.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let m=T(u),h=new i.StdioServerTransport;await m.connect(h),console.error(`Chrome extension MCP server started on stdio`);let g=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{l.clearAllTasks(`Server shutting down`),await h.close(),p.close(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>g(`SIGINT`)),process.on(`SIGTERM`,()=>g(`SIGTERM`))}catch(e){let t=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${t}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),D=`BROWSE_TOOL_CONFIG`,O=`.browse-tool`,k=`config.json`,ee=[`json`,`text`,`quiet`],te={commands:{},tools:{}},ne=m.z.record(m.z.string(),m.z.unknown()),re=m.z.object({mcpServe:m.z.object({type:m.z.string().optional(),browser:m.z.string().optional(),headless:m.z.boolean().optional(),profile:m.z.string().optional(),mode:m.z.string().optional(),host:m.z.string().optional(),port:m.z.coerce.number().int().positive().optional(),tags:m.z.string().optional(),exclude:m.z.string().optional(),customTools:m.z.string().optional(),snippetsDir:m.z.string().optional(),registryPath:m.z.string().optional(),registryDir:m.z.string().optional(),pidsDir:m.z.string().optional(),profilesDir:m.z.string().optional()}).partial().optional(),httpServe:m.z.object({port:m.z.coerce.number().int().positive().optional(),headless:m.z.boolean().optional(),idleTimeout:m.z.coerce.number().positive().optional(),host:m.z.string().optional(),registryDir:m.z.string().optional(),registryPath:m.z.string().optional(),pidsDir:m.z.string().optional(),profilesDir:m.z.string().optional(),snippetsDir:m.z.string().optional()}).partial().optional(),exec:m.z.object({format:m.z.enum(ee).optional(),color:m.z.boolean().optional(),port:m.z.coerce.number().int().positive().optional()}).partial().optional(),tools:m.z.object({format:m.z.enum(ee).optional(),color:m.z.boolean().optional(),port:m.z.coerce.number().int().positive().optional()}).partial().optional(),status:m.z.record(m.z.string(),m.z.unknown()).optional(),stop:m.z.record(m.z.string(),m.z.unknown()).optional()}),ie=m.z.object({commands:re.default({}),tools:m.z.record(m.z.string(),ne).default({})});let A={config:te};function ae(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(n===`--config`)return e[t+1];if(n.startsWith(`--config=`))return n.slice(9)}}function oe(e){let t=u.default.resolve(e);if(!(0,l.existsSync)(t))throw Error(`Config path not found: ${t}`);return(0,l.statSync)(t).isDirectory()?u.default.join(t,k):t}function se(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??(0,p.homedir)(),a=ae(e);if(a)return oe(a);if(n[D])return oe(n[D]);let o=u.default.join(r,O,k);if((0,l.existsSync)(o))return o;let s=u.default.join(i,O,k);if((0,l.existsSync)(s))return s}function ce(e){let t=(0,l.readFileSync)(e,`utf8`),n=JSON.parse(t);return ie.parse(n)}function le(e,t={}){let n=se(e,t);return n?{configPath:n,config:ce(n)}:{config:te}}function ue(e,t={}){return A=le(e,t),A}function j(e){return(A.config.commands??{})[e]??{}}function de(e){return A.config.tools?.[e]??{}}function M(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}const fe=6e4;var pe=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??3200,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await e.o().get(e.C.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!t.running||!t.port)throw Error(t.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=t.port,t.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function N(e){return new pe(e)}const me={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function P(e,t,n){return n?`${me[t]}${e}${me.reset}`:e}function he(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?ge(e):_e(e,r)}function ge(e){return JSON.stringify(e,null,2)}function _e(e,t){let n=[];e.isError&&n.push(P(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(ve(r,t)):r.type===`image`?n.push(xe(r,t)):n.push(P(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
|
-
`)}function
|
|
4
|
-
`)}function
|
|
5
|
-
`)}function Ce(e,t){let n=j(`tools`),r=M(e,`format`,t.format,n.format),i=M(e,`color`,t.color,n.color);return{port:M(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const we=new t.Command(`list-custom-tools`).description(`List custom tools from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.l)).action(async function(e,t){let{port:n,formatterOptions:r}=Ce(this,t);try{let t=await N({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?Se(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(F(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),Te=new t.Command(`exec-custom-tool`).description(`Execute a custom tool from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).argument(`<tool>`,`Custom tool name to execute`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.l)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=Ce(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(F(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await N({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=he(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(F(e instanceof Error?e:String(e),a.color)),process.exit(1)}}),Ee=new t.Command(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(we).addCommand(Te),De=new t.Command(`docker-build-cft`).description(`Build the Chrome for Testing Docker image used by vm mode`).option(`--cft-version <version>`,`Chrome for Testing version to build`,e.v).option(`--image <image>`,`Docker image tag to produce`,e.g).option(`--platform <platform>`,`Docker target platform`,e._).action(async t=>{try{let n=await e.m({version:t.cftVersion,image:t.image,platform:t.platform,stdio:`inherit`});console.log(`Built Docker image ${n.image}`),console.log(` Version: ${n.version}`),console.log(` Platform: ${n.platform}`),console.log(` Archive: ${e.h(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Oe=new t.Command(`exec`).description(`Execute a tool directly with JSON arguments`).argument(`<tool>`,`Tool name to execute (e.g., browser_launch)`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.l)).action(async function(e,t,n){let r=j(`exec`),i=M(this,`format`,n.format,r.format),a=M(this,`color`,n.color,r.color),o=M(this,`port`,n.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),s=this.getOptionValueSourceWithGlobals(`port`),c=s!==void 0&&s!==`default`&&s!==`implied`,l={format:i,color:a};try{let n;try{n=JSON.parse(t)}catch{console.error(F(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await N({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=he(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(F(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),ke=`tools.yaml`,I=`pageId`,L=`browserId`,Ae=m.z.record(m.z.string(),m.z.unknown()),je=m.z.object({type:m.z.literal(`object`),properties:m.z.record(m.z.string(),m.z.unknown()).optional(),required:m.z.array(m.z.string()).optional(),additionalProperties:m.z.boolean().optional()}).passthrough(),Me=m.z.object({name:m.z.string().min(1),description:m.z.string().min(1),script:m.z.string().min(1),suggestionActions:m.z.string().min(1).optional(),capabilities:Ae,inputSchema:je}),Ne=m.z.object({tools:m.z.array(Me)}),Pe=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`,Fe=1500,Ie=50;function Le(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function R(e,t){if(Pe){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function z(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Re(e){return z(e)&&Array.isArray(e.content)}function B(e){let t=e.trim();return t===``?``:t.startsWith(`"`)&&t.endsWith(`"`)||t.startsWith(`[`)&&t.endsWith(`]`)||t.startsWith(`{`)&&t.endsWith(`}`)?JSON.parse(t):t.startsWith(`'`)&&t.endsWith(`'`)?t.slice(1,-1):t===`true`?!0:t===`false`?!1:t===`null`?null:/^-?\d+(\.\d+)?$/.test(t)?Number(t):t}function ze(e){if(!e||Object.keys(e).length===0)return;let t={};for(let[n,r]of Object.entries(e))if(r!=null){if(typeof r==`string`||typeof r==`number`||typeof r==`boolean`){t[n]=r;continue}t[n]=JSON.stringify(r)}return Object.keys(t).length>0?t:void 0}function Be(e){return e.replace(/^\uFEFF/,``).split(/\r?\n/).map(e=>{if(e.includes(` `))throw Error(`Tab indentation is not supported in tools.yaml`);let t=e.replace(/\s+#.*$/,``);return t.trim().length===0?null:{indent:t.match(/^ */)?.[0].length??0,text:t.trim()}}).filter(e=>e!==null)}function V(e,t,n){let r=e[t];if(!r||r.indent!==n)throw Error(`Invalid indentation in tools.yaml at line ${t+1}`);return r.text.startsWith(`-`)?Ve(e,t,n):He(e,t,n)}function Ve(e,t,n){let r=[],i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||!t.text.startsWith(`-`))break;let a=t.text.slice(1).trim();if(a===``){let t=e[i+1];if(!t||t.indent<=n){r.push(null),i+=1;continue}let[a,o]=V(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=H(a),s={};if(o===void 0){let r=e[i+1];if(!r||r.indent<=n)throw Error(`Expected nested value for "${t}" in tools.yaml`);let[a,o]=V(e,i+1,r.indent);s[t]=a,i=o}else s[t]=B(o),i+=1;for(;i<e.length&&e[i].indent>n;){let t=e[i];if(t.indent!==n+2||t.text.startsWith(`-`)){let[n,r]=V(e,i,t.indent);if(!z(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=H(t.text);if(a===void 0){let n=e[i+1];if(!n||n.indent<=t.indent)throw Error(`Expected nested value for "${r}" in tools.yaml`);let[a,o]=V(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=B(a),i+=1}r.push(s);continue}r.push(B(a)),i+=1}return[r,i]}function He(e,t,n){let r={},i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||t.text.startsWith(`-`))break;let[a,o]=H(t.text);if(o===void 0){let t=e[i+1];if(!t||t.indent<=n){r[a]=null,i+=1;continue}let[o,s]=V(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=B(o),i+=1}return[r,i]}function H(e){let t=e.indexOf(`:`);if(t===-1)throw Error(`Invalid tools.yaml entry: "${e}"`);let n=e.slice(0,t).trim(),r=e.slice(t+1).trim();return[n,r===``?void 0:r]}function Ue(e){let t=Be(e);if(t.length===0)return{};let[n]=V(t,0,t[0].indent);return n}function We(e){let t=e.inputSchema.properties;if(!t||!z(t))throw Error(`Custom tool "${e.name}" must define inputSchema.properties`);let n=t[I],r=t[L],i=n!==void 0&&z(n)&&n.type===`string`,a=r!==void 0&&z(r)&&r.type===`string`;if(!i&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId and/or inputSchema.properties.browserId as type "string"`);if(n!==void 0&&!i)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`)}async function Ge(e){let t=(0,g.stripTypeScriptTypes)(await(0,h.readFile)(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function Ke(e,t){let n=typeof t.run==`function`?t.run:void 0;if(!n)throw Error(`Custom tool script "${e}" must export a "run" function`);return{execute:n}}function qe(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function Je(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(z(r)){let i=qe(r,t);return{...e,content:[{...n,text:JSON.stringify(i,null,2)},...e.content.slice(1)]}}}catch{}return{...e,content:[...e.content,{type:`text`,text:`suggestionActions: ${t}`}]}}function Ye(e,t,n){if(Re(t))return n?Je(t,n):t;if(typeof t==`string`)return n?{content:[{type:`text`,text:JSON.stringify({result:t,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:t}]};if(t===void 0)return n?{content:[{type:`text`,text:JSON.stringify({result:`Custom tool "${e}" completed successfully`,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:`Custom tool "${e}" completed successfully`}]};let r=z(t)&&n?qe(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var Xe=class{constructor(t,n,r=new e.x,i){this.pageRegistry=t,this.extensionTaskQueue=n,this.telemetry=r,this.browserService=i}resolveToolPage(t,n,r){if(r.page)return r.page;if(r.mode===`extension`&&this.extensionTaskQueue){let t=new e.p(this.extensionTaskQueue);return t.setTarget(n,r.browserId),t}throw Error(`Custom tool "${t}" requires a supported page context`)}toPageSummary(e,t){return{pageId:t.id,url:t.url,title:t.title,active:e===t.id}}async createPageForBrowser(e,t,n){if(!this.browserService)throw Error(`Custom tool "${e}" requires browser service support to create a page`);let r=this.browserService.getBrowser(t);if(!r)throw Error(`Browser "${t}" not found`);let i=n?.setAsCurrent!==!1;if(r.mode===`extension`||r.mode===`vm`){if(!this.extensionTaskQueue)throw Error(`Custom tool "${e}" requires extension task support to create a page`);let a=this.pageRegistry.registerExtensionPage(t,void 0,n?.url,!1);try{let o=await this.extensionTaskQueue.queueTask(`browser_new_page`,{browserId:t,pageId:a,url:n?.url,setAsCurrent:i},1e4,t);if(!o.success)throw Error(o.error??`Custom tool "${e}" failed to create a page`);let s=o.result?.content[0]?.type===`text`?o.result.content[0].text:void 0,c={};if(typeof s==`string`&&s.length>0)try{c=JSON.parse(s)}catch{c={}}let l=this.pageRegistry.get(a);if(!l)throw Error(`Page "${a}" was not registered`);l.url=c.url??l.url,l.title=c.title??l.title,l.extensionTabId=c.tabId??l.extensionTabId;let u=await this.waitForResolvedPageMetadata(a);return r.pageIds.add(a),(i||!r.currentPageId)&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a),{...this.toPageSummary(r.currentPageId,u),page:this.resolveToolPage(e,a,u)}}catch(e){throw this.pageRegistry.remove(a),e}}let{pageId:a,page:o}=await this.browserService.newPage(t);n?.url&&(await o.goto(n.url),await this.pageRegistry.updateMetadata(a)),i&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a);let s=this.pageRegistry.get(a);if(!s)throw Error(`Page "${a}" was not registered`);return{...this.toPageSummary(r.currentPageId,s),page:this.resolveToolPage(e,a,s)}}async waitForResolvedPageMetadata(e){let t=Date.now(),n=this.pageRegistry.get(e);for(;n&&Date.now()-t<1500;){let t=typeof n.url==`string`&&n.url.length>0,r=typeof n.title==`string`&&n.title.length>0&&n.title!==`Extension Tab`;if(t&&r)return n;await new Promise(e=>setTimeout(e,50)),n=this.pageRegistry.get(e)}if(!n)throw Error(`Page "${e}" was not registered`);return n}async resolveExecutionContext(e,t){let n=typeof t[I]==`string`&&t[I].length>0?t[I]:void 0,r=typeof t[L]==`string`&&t[L].length>0?t[L]:void 0;if(!n&&!r)throw Error(`Custom tool "${e}" requires a string pageId or browserId`);if(n){let t=this.pageRegistry.get(n);if(!t)throw Error(`Page "${n}" not found`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}let i=this.createBrowserHelper(e,r);try{let e=await i.getCurrentPage(),t=this.pageRegistry.get(e.pageId);if(!t)throw Error(`Page "${e.pageId}" not found`);return{pageId:e.pageId,pageEntry:t,page:e.page,browser:i}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await i.newPage(),n=this.pageRegistry.get(t.pageId);if(!n)throw Error(`Page "${t.pageId}" not found`,{cause:e});return{pageId:t.pageId,pageEntry:n,page:t.page,browser:i}}}createBrowserHelper(e,t){return{browserId:t,mode:this.browserService?.getBrowser(t)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(t);if(!e)throw Error(`Browser "${t}" not found`);return this.pageRegistry.findByBrowser(t).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async n=>{let r=this.pageRegistry.get(n);if(!r||r.browserId!==t)throw Error(`Page "${n}" not found in browser "${t}"`);let i=this.browserService?.getBrowser(t);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(e,n,r)}},getCurrentPage:async()=>{let n=this.browserService?.getBrowser(t);if(!n)throw Error(`Browser "${t}" not found`);let r=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},newPage:async n=>this.createPageForBrowser(e,t,n)}}createToolLogger(e,t,n){let r={"browse_tool.tool.name":e,"browse_tool.page.id":t,"browse_tool.browser.id":n.browserId,"browse_tool.execution.mode":n.mode},i=(e,t,n)=>{this.telemetry.log(e,t,{attributes:ze({...r,...n?.attributes??{}}),exception:n?.exception})};return{getTraceContext:()=>this.telemetry.getActiveTraceContext(),trace:(e,t)=>i(`trace`,e,t),debug:(e,t)=>i(`debug`,e,t),info:(e,t)=>i(`info`,e,t),warn:(e,t)=>i(`warn`,e,t),error:(e,t)=>i(`error`,e,t),fatal:(e,t)=>i(`fatal`,e,t)}}async listTools(e){return(await this.loadTools(e)).map(({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i})=>({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i}))}async executeTool(e,t,n){let r=typeof n[I]==`string`?n[I]:void 0,i=typeof n[L]==`string`?n[L]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":u.default.resolve(e),"browse_tool.page.id":r,"browse_tool.browser.id":i}},async r=>{let i=(await this.loadTools(e)).find(e=>e.name===t);if(!i)throw r?.setStatus({code:d.SpanStatusCode.ERROR,message:`Custom tool "${t}" not found`}),Error(`Custom tool "${t}" not found`);let{pageId:a,pageEntry:o,page:s,browser:c}=await this.resolveExecutionContext(t,n),l=this.createToolLogger(t,a,o);r?.setAttributes({"browse_tool.browser.id":o.browserId,"browse_tool.execution.mode":o.mode,"browse_tool.page.id":a}),R(`Executing custom tool`,{toolName:t,directory:u.default.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:Le(n),scriptPath:i.scriptPath});let f;try{f=await i.execute?.({page:s,browser:c,input:n,logger:l})}catch(e){let n=e instanceof Error?e.message:String(e);throw R(`Custom tool execution failed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,error:n,stack:e instanceof Error?e.stack:void 0}),Error(`Custom tool "${t}" failed on page "${a}": ${n}`,{cause:e})}R(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:Le(f)});let p=Ye(t,f,i.suggestionActions),m=p.isError?p.content[0]?.text:void 0;return m&&r?.setStatus({code:d.SpanStatusCode.ERROR,message:m}),p})}async loadTools(e){let t=u.default.resolve(e),n=u.default.join(t,`tools.yaml`),r=Ue(await(0,h.readFile)(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=Ne.parse(r);return Promise.all(i.tools.map(async e=>{We(e);let n=u.default.resolve(t,e.script);await(0,h.stat)(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=Ke(n,await Ge(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function Ze(t){let n=new o.Hono;return n.get(`/browsers`,async n=>{try{let r=t.get(e.C.BrowserService),i=t.get(e.C.PageRegistry),a=r.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString(),pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt.toISOString()}))}}),c={totalBrowsers:a.length,totalPages:o.length};return n.json({browsers:s,stats:c})}catch(e){return console.error(`Failed to list browsers:`,e),n.json({error:`Failed to list browsers`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/browsers`,async n=>{try{return await t.get(e.C.BrowserService).closeAll(),n.json({success:!0,message:`All browsers closed`})}catch(e){return console.error(`Failed to close all browsers:`,e),n.json({error:`Failed to close all browsers`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/browsers/:id`,async n=>{try{let r=n.req.param(`id`),i=t.get(e.C.BrowserService);return i.getBrowser(r)?(await i.closeBrowser(r),n.json({success:!0,message:`Browser "${r}" closed`})):n.json({error:`Browser "${r}" not found`},404)}catch(e){return console.error(`Failed to close browser:`,e),n.json({error:`Failed to close browser`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/pages/:id`,async n=>{try{let r=n.req.param(`id`),i=t.get(e.C.PageRegistry).get(r);return i?i.page?(await i.page.close(),n.json({success:!0,message:`Page "${r}" closed`})):n.json({error:`Page "${r}" is in extension mode and cannot be closed via API`},400):n.json({error:`Page "${r}" not found`},404)}catch(e){return console.error(`Failed to close page:`,e),n.json({error:`Failed to close page`,message:e instanceof Error?e.message:String(e)},500)}}),n}function Qe(e){return e.toLocaleString(`en-US`,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1})}function $e(e){let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60);return r>0?`${r}h ${n%60}m`:n>0?`${n}m ${t%60}s`:`${t}s`}function et(e){return $e(new Date().getTime()-e.getTime())}const tt=`dashboard-container`,nt=`dashboard-header`,rt=`btn`,it=`btn-danger`,U=`table-container`,at=`browser-table`,ot=`stats-container`,st=`stat-card`,ct=`browser-row`,lt=`page-row`,W=`status-active`,ut=`url-cell`,G=`timestamp-cell`,K=`actions-cell`,q=`kill-btn`,dt=`refresh-btn`,ft=`empty-state`,pt=`
|
|
2
|
+
const e=require(`./streamable-http-BAKht810.cjs`);require(`./playwright-test-jbz0wL4w.cjs`),require(`reflect-metadata/lite`);let t=require(`commander`),n=require(`@hono/node-server`),r=require(`@agimon-ai/foundation-process-registry`),i=require(`@agimon-ai/foundation-port-registry`),a=require(`@modelcontextprotocol/sdk/server/index.js`),o=require(`@modelcontextprotocol/sdk/server/stdio.js`),s=require(`@modelcontextprotocol/sdk/types.js`),c=require(`node:fs`),l=require(`node:path`);l=e.w(l);let u=require(`hono`),d=require(`hono/cors`),f=require(`inversify`),p=require(`@opentelemetry/api`),m=require(`node:os`),h=require(`zod`),g=require(`node:fs/promises`),_=require(`node:module`),v=require(`@hono/node-ws`),y=require(`hono/html`),b=require(`hono/jsx/jsx-runtime`);var x=`0.2.13`;const S=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function C(e,t){if(!S)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function w(t){let n=new u.Hono,r;try{r=t.get(e.C.TelemetryService)}catch{r=new e.x}return n.get(`/telemetry-config`,t=>{try{let n=new URL(t.req.url).origin;return t.json(e.S(process.env,n))}catch(e){return t.json({enabled:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tasks`,n=>{try{let r=t.get(e.C.ExtensionTaskQueue).getNextTask();return r?n.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):n.json({})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/result`,async n=>{try{let r=await n.req.json();if(!r.taskId)return n.json({success:!1,error:`Missing taskId in request body`},400);let i=t.get(e.C.ExtensionTaskQueue),a=t.get(e.C.BrowserService),o={taskId:r.taskId,success:r.success,result:r.result,error:r.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),n.json({success:!0})):n.json({success:!1,error:`Task ${r.taskId} not found or already completed`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/status`,n=>{try{let r=t.get(e.C.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.queueSize};return n.json(a)}catch(e){return n.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/register`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).register(r);return n.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/heartbeat`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).heartbeat(r);return i?n.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/tab-mapped`,async n=>{try{let r=await n.req.json();if(!r.pageId||typeof r.tabId!=`number`)return n.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=t.get(e.C.PageRegistry),a=t.get(e.C.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),n.json({success:!0})):n.json({success:!1,error:`Page ${r.pageId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording`,async n=>{try{let r=await n.req.json();if(!r.browserId)return n.json({success:!1,error:`Missing browserId in request body`},400);let i=t.get(e.C.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return C(`artifact received`,{browserId:r.browserId,videoBase64Size:r.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(r.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${r.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/recording/chunk`,async n=>{try{let i=await n.req.json();if(!i.browserId||!i.chunkBase64)return n.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=t.get(e.C.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return C(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(r.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":i.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof i.chunkIndex==`number`?{"browse_tool.recording.chunk_index":i.chunkIndex}:{},...typeof i.mimeType==`string`?{"browse_tool.recording.mime_type":i.mimeType}:{}}}),a.recordBrowserActivity(i.browserId),n.json({success:!0})):n.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/browser-log`,async e=>{try{let t=await e.req.json();return!t.message||typeof t.message!=`string`?e.json({success:!1,error:`Missing message in request body`},400):(r.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),C(`browser log relayed`,{level:t.level??`info`,message:t.message,attributes:t.attributes}),e.json({success:!0}))}catch(t){return e.json({success:!1,error:t instanceof Error?t.message:String(t)},500)}}),n.post(`/handoff`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).requestHandoff(r);return i?n.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.post(`/handoff/acknowledge`,async n=>{try{let r=await n.req.json();if(!r.sessionId)return n.json({success:!1,error:`Missing sessionId in request body`},400);let i=t.get(e.C.ExtensionSessionRegistry).acknowledgeHandoff(r.sessionId);return i?n.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):n.json({success:!1,error:`Session ${r.sessionId} not found or no handoff pending`},404)}catch(e){return n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/sessions`,n=>{try{let r=t.get(e.C.ExtensionSessionRegistry).listSessions();return n.json({sessions:r.map(e=>({id:e.id,browserId:e.browserId,tabId:e.tabId,currentUrl:e.currentUrl,controlMode:e.controlMode,activeSpecPath:e.activeSpecPath,handoffRequested:e.handoffRequested,createdAt:e.createdAt.toISOString(),lastHeartbeatAt:e.lastHeartbeatAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n}function T(e){if(e==null||e===``)return;let t=typeof e==`number`?e:typeof e==`string`?Number.parseInt(e,10):NaN;if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}const E=[`pnpm-workspace.yaml`,`nx.json`,`.git`],D=`browse-tool-chrome`,O=`tool`,k=process.env.NODE_ENV||`development`,ee=`127.0.0.1`;function te(e=process.cwd()){let t=l.default.resolve(e);for(;;){for(let e of E)if((0,c.existsSync)(l.default.join(t,e)))return t;let e=l.default.dirname(t);if(e===t)return process.cwd();t=e}}function ne(e,t){if(!e)return;let n=l.default.resolve(e);return l.default.extname(n)===`.json`?l.default.join(l.default.dirname(n),t):l.default.join(n,t)}function re(){return new i.PortRegistryService(process.env.PORT_REGISTRY_PATH)}function ie(){return new r.ProcessRegistryService(process.env.PROCESS_REGISTRY_PATH)}async function ae(e,t,n){let r=re(),i=await r.reservePort({repositoryPath:e,serviceName:D,serviceType:O,environment:k,preferredPort:t,portRange:n,pid:process.pid,host:ee,force:!0,metadata:{transport:`stdio`,mode:`chrome-serve`}});if(!i.success||!i.record)throw Error(i.error||`Failed to reserve port ${t}`);let a=!1;return{port:i.record.port,release:async()=>{if(a)return;a=!0;let n=await r.releasePort({repositoryPath:e,serviceName:D,serviceType:O,environment:k,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}async function oe(e,t,n){let r=ie(),i=await r.registerProcess({repositoryPath:e,serviceName:D,serviceType:O,environment:k,pid:process.pid,port:n,host:ee,command:process.argv[1],args:process.argv.slice(2),metadata:t,force:!0});if(!i.success||!i.record)throw Error(i.error||`Failed to register process`);let a=!1;return{release:async()=>{if(a)return;a=!0;let t=await r.releaseProcess({repositoryPath:e,serviceName:D,serviceType:O,environment:k,pid:process.pid,kill:!1,releasePort:!1});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error||`Failed to release process`)}}}function se(){return new f.ContainerModule(t=>{t.bind(e.C.ExtensionTaskQueue).to(e.b).inSingletonScope(),t.bind(e.C.ExtensionToolDelegator).to(e.y).inSingletonScope()})}function ce(e){let t=new a.Server({name:`browse-tool-chrome`,version:`0.1.0`},{capabilities:{tools:{}}}),n=e.getSupportedTools().map(e=>({name:e,description:`Browser automation tool (Chrome extension mode): ${e}`,inputSchema:{type:`object`,properties:{},additionalProperties:!0}}));return t.setRequestHandler(s.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(s.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const le=new t.Command(`chrome-serve`).description(`[DEPRECATED] Start MCP server with Chrome extension HTTP polling for bot-detection-free browser automation`).option(`-p, --port <port>`,`HTTP server port for extension polling`).option(`-v, --verbose`,`Enable verbose output`,!1).option(`--wait-for-extension`,`Wait for extension to connect before accepting MCP requests`,!1).action(async t=>{console.error(``),console.error(`╔════════════════════════════════════════════════════════════════╗`),console.error(`║ DEPRECATED: chrome-serve is deprecated. ║`),console.error(`║ Use mcp-serve instead - extension routes are now ║`),console.error(`║ automatically available in the HTTP server at /extension/*. ║`),console.error(`╚════════════════════════════════════════════════════════════════╝`),console.error(``);let r,a;try{let s=T(t.port),c=te(process.cwd()),l=process.env.PORT_REGISTRY_PATH;l&&(process.env.PROCESS_REGISTRY_PATH=ne(l,`processes.json`)),r=await ae(c,s??i.DEFAULT_PORT_RANGE.min,s?{min:s,max:s}:i.DEFAULT_PORT_RANGE);let p=r.port;a=await oe(c,{transport:`stdio`,command:`chrome-serve`,waitForExtension:t.waitForExtension},p),t.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${p}`),console.error(` Wait for extension: ${t.waitForExtension}`));let m=new f.Container({defaultScope:`Singleton`});m.load(se());let h=m.get(e.C.ExtensionTaskQueue),g=m.get(e.C.ExtensionToolDelegator),_=new u.Hono;_.use(`*`,(0,d.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let v=w(m);_.route(`/extension`,v),_.get(`/health`,e=>{let t=h.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let y=(0,n.serve)({fetch:_.fetch,port:p});y.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${p} is already in use.`),console.error(`Recovery: Try a different port with --port <port>`)):console.error(`Error [SERVER_ERROR]: HTTP server error: ${e.message}`),process.exit(1)}),console.error(`HTTP server started on port ${p}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${p}/extension/tasks`),t.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{h.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let b=ce(g),x=new o.StdioServerTransport;await b.connect(x),console.error(`Chrome extension MCP server started on stdio`);let S=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{h.clearAllTasks(`Server shutting down`),await x.close(),y.close(),await a.release(),await r.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>S(`SIGINT`)),process.on(`SIGTERM`,()=>S(`SIGTERM`))}catch(e){if(a||r)try{await a?.release(),await r?.release()}catch{}let t=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${t}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),ue=`BROWSE_TOOL_CONFIG`,de=`.browse-tool`,A=`config.json`,fe=[`json`,`text`,`quiet`],pe={commands:{},tools:{}},me=h.z.record(h.z.string(),h.z.unknown()),he=h.z.object({mcpServe:h.z.object({type:h.z.string().optional(),browser:h.z.string().optional(),headless:h.z.boolean().optional(),profile:h.z.string().optional(),mode:h.z.string().optional(),host:h.z.string().optional(),port:h.z.coerce.number().int().positive().optional(),tags:h.z.string().optional(),exclude:h.z.string().optional(),customTools:h.z.string().optional(),snippetsDir:h.z.string().optional(),registryPath:h.z.string().optional(),registryDir:h.z.string().optional(),pidsDir:h.z.string().optional(),profilesDir:h.z.string().optional()}).partial().optional(),httpServe:h.z.object({port:h.z.coerce.number().int().positive().optional(),headless:h.z.boolean().optional(),idleTimeout:h.z.coerce.number().positive().optional(),host:h.z.string().optional(),registryDir:h.z.string().optional(),registryPath:h.z.string().optional(),pidsDir:h.z.string().optional(),profilesDir:h.z.string().optional(),snippetsDir:h.z.string().optional()}).partial().optional(),exec:h.z.object({format:h.z.enum(fe).optional(),color:h.z.boolean().optional(),port:h.z.coerce.number().int().positive().optional()}).partial().optional(),tools:h.z.object({format:h.z.enum(fe).optional(),color:h.z.boolean().optional(),port:h.z.coerce.number().int().positive().optional()}).partial().optional(),status:h.z.record(h.z.string(),h.z.unknown()).optional(),stop:h.z.record(h.z.string(),h.z.unknown()).optional()}),ge=h.z.object({commands:he.default({}),tools:h.z.record(h.z.string(),me).default({})});let j={config:pe};function _e(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(n===`--config`)return e[t+1];if(n.startsWith(`--config=`))return n.slice(9)}}function ve(e){let t=l.default.resolve(e);if(!(0,c.existsSync)(t))throw Error(`Config path not found: ${t}`);return(0,c.statSync)(t).isDirectory()?l.default.join(t,A):t}function ye(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??(0,m.homedir)(),a=_e(e);if(a)return ve(a);if(n[ue])return ve(n[ue]);let o=l.default.join(r,de,A);if((0,c.existsSync)(o))return o;let s=l.default.join(i,de,A);if((0,c.existsSync)(s))return s}function be(e){let t=(0,c.readFileSync)(e,`utf8`),n=JSON.parse(t);return ge.parse(n)}function xe(e,t={}){let n=ye(e,t);return n?{configPath:n,config:be(n)}:{config:pe}}function Se(e,t={}){return j=xe(e,t),j}function M(e){return(j.config.commands??{})[e]??{}}function Ce(e){return j.config.tools?.[e]??{}}function N(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}const we=6e4;var Te=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??3200,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await e.o().get(e.C.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!t.running||!t.port)throw Error(t.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=t.port,t.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function P(e){return new Te(e)}const Ee={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function F(e,t,n){return n?`${Ee[t]}${e}${Ee.reset}`:e}function I(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?De(e):Oe(e,r)}function De(e){return JSON.stringify(e,null,2)}function Oe(e,t){let n=[];e.isError&&n.push(F(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(ke(r,t)):r.type===`image`?n.push(Me(r,t)):n.push(F(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
|
+
`)}function ke(e,t){let n=e.text;try{return Ae(JSON.parse(n),t)}catch{return n}}function Ae(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=F(r,`cyan`,t),a=je(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
|
+
`)}function je(e,t){return e===null?F(`null`,`gray`,t):e===void 0?F(`undefined`,`gray`,t):typeof e==`boolean`?F(String(e),e?`green`:`red`,t):typeof e==`number`?F(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?F(e,`blue`,t):e:Array.isArray(e)?e.length===0?F(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function Me(e,t){let{mimeType:n,data:r}=e;return F(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function L(e,t){return F(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function Ne(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=F(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
|
+
`)}function Pe(e,t){let n=M(`tools`),r=N(e,`format`,t.format,n.format),i=N(e,`color`,t.color,n.color);return{port:N(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Fe=new t.Command(`list-custom-tools`).description(`List custom tools from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.l)).action(async function(e,t){let{port:n,formatterOptions:r}=Pe(this,t);try{let t=await P({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?Ne(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(L(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),Ie=new t.Command(`exec-custom-tool`).description(`Execute a custom tool from a tools directory`).argument(`<dir>`,`Path to the tools directory containing tools.yaml`).argument(`<tool>`,`Custom tool name to execute`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.l)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=Pe(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(L(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await P({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=I(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(L(e instanceof Error?e:String(e),a.color)),process.exit(1)}}),Le=new t.Command(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Fe).addCommand(Ie),Re=new t.Command(`docker-build-cft`).description(`Build the Chrome for Testing Docker image used by vm mode`).option(`--cft-version <version>`,`Chrome for Testing version to build`,e.v).option(`--image <image>`,`Docker image tag to produce`,e.g).option(`--platform <platform>`,`Docker target platform`,e._).action(async t=>{try{let n=await e.m({version:t.cftVersion,image:t.image,platform:t.platform,stdio:`inherit`});console.log(`Built Docker image ${n.image}`),console.log(` Version: ${n.version}`),console.log(` Platform: ${n.platform}`),console.log(` Archive: ${e.h(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),ze=new t.Command(`exec`).description(`Execute a tool directly with JSON arguments`).argument(`<tool>`,`Tool name to execute (e.g., browser_launch)`).argument(`[args]`,`JSON arguments for the tool`,`{}`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.l)).action(async function(e,t,n){let r=M(`exec`),i=N(this,`format`,n.format,r.format),a=N(this,`color`,n.color,r.color),o=N(this,`port`,n.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),s=this.getOptionValueSourceWithGlobals(`port`),c=s!==void 0&&s!==`default`&&s!==`implied`,l={format:i,color:a};try{let n;try{n=JSON.parse(t)}catch{console.error(L(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await P({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=I(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(L(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),Be=`tools.yaml`,R=`pageId`,z=`browserId`,Ve=h.z.record(h.z.string(),h.z.unknown()),He=h.z.object({type:h.z.literal(`object`),properties:h.z.record(h.z.string(),h.z.unknown()).optional(),required:h.z.array(h.z.string()).optional(),additionalProperties:h.z.boolean().optional()}).passthrough(),Ue=h.z.object({name:h.z.string().min(1),description:h.z.string().min(1),script:h.z.string().min(1),suggestionActions:h.z.string().min(1).optional(),capabilities:Ve,inputSchema:He}),We=h.z.object({tools:h.z.array(Ue)}),Ge=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`,Ke=1500,qe=50;function Je(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function B(e,t){if(Ge){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function V(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ye(e){return V(e)&&Array.isArray(e.content)}function H(e){let t=e.trim();return t===``?``:t.startsWith(`"`)&&t.endsWith(`"`)||t.startsWith(`[`)&&t.endsWith(`]`)||t.startsWith(`{`)&&t.endsWith(`}`)?JSON.parse(t):t.startsWith(`'`)&&t.endsWith(`'`)?t.slice(1,-1):t===`true`?!0:t===`false`?!1:t===`null`?null:/^-?\d+(\.\d+)?$/.test(t)?Number(t):t}function Xe(e){if(!e||Object.keys(e).length===0)return;let t={};for(let[n,r]of Object.entries(e))if(r!=null){if(typeof r==`string`||typeof r==`number`||typeof r==`boolean`){t[n]=r;continue}t[n]=JSON.stringify(r)}return Object.keys(t).length>0?t:void 0}function Ze(e){return e.replace(/^\uFEFF/,``).split(/\r?\n/).map(e=>{if(e.includes(` `))throw Error(`Tab indentation is not supported in tools.yaml`);let t=e.replace(/\s+#.*$/,``);return t.trim().length===0?null:{indent:t.match(/^ */)?.[0].length??0,text:t.trim()}}).filter(e=>e!==null)}function U(e,t,n){let r=e[t];if(!r||r.indent!==n)throw Error(`Invalid indentation in tools.yaml at line ${t+1}`);return r.text.startsWith(`-`)?Qe(e,t,n):$e(e,t,n)}function Qe(e,t,n){let r=[],i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||!t.text.startsWith(`-`))break;let a=t.text.slice(1).trim();if(a===``){let t=e[i+1];if(!t||t.indent<=n){r.push(null),i+=1;continue}let[a,o]=U(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=et(a),s={};if(o===void 0){let r=e[i+1];if(!r||r.indent<=n)throw Error(`Expected nested value for "${t}" in tools.yaml`);let[a,o]=U(e,i+1,r.indent);s[t]=a,i=o}else s[t]=H(o),i+=1;for(;i<e.length&&e[i].indent>n;){let t=e[i];if(t.indent!==n+2||t.text.startsWith(`-`)){let[n,r]=U(e,i,t.indent);if(!V(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=et(t.text);if(a===void 0){let n=e[i+1];if(!n||n.indent<=t.indent)throw Error(`Expected nested value for "${r}" in tools.yaml`);let[a,o]=U(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=H(a),i+=1}r.push(s);continue}r.push(H(a)),i+=1}return[r,i]}function $e(e,t,n){let r={},i=t;for(;i<e.length;){let t=e[i];if(t.indent<n||t.indent!==n||t.text.startsWith(`-`))break;let[a,o]=et(t.text);if(o===void 0){let t=e[i+1];if(!t||t.indent<=n){r[a]=null,i+=1;continue}let[o,s]=U(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=H(o),i+=1}return[r,i]}function et(e){let t=e.indexOf(`:`);if(t===-1)throw Error(`Invalid tools.yaml entry: "${e}"`);let n=e.slice(0,t).trim(),r=e.slice(t+1).trim();return[n,r===``?void 0:r]}function tt(e){let t=Ze(e);if(t.length===0)return{};let[n]=U(t,0,t[0].indent);return n}function nt(e){let t=e.inputSchema.properties;if(!t||!V(t))throw Error(`Custom tool "${e.name}" must define inputSchema.properties`);let n=t[R],r=t[z],i=n!==void 0&&V(n)&&n.type===`string`,a=r!==void 0&&V(r)&&r.type===`string`;if(!i&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId and/or inputSchema.properties.browserId as type "string"`);if(n!==void 0&&!i)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!a)throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`)}async function rt(e){let t=(0,_.stripTypeScriptTypes)(await(0,g.readFile)(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function it(e,t){let n=typeof t.run==`function`?t.run:void 0;if(!n)throw Error(`Custom tool script "${e}" must export a "run" function`);return{execute:n}}function at(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function ot(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(V(r)){let i=at(r,t);return{...e,content:[{...n,text:JSON.stringify(i,null,2)},...e.content.slice(1)]}}}catch{}return{...e,content:[...e.content,{type:`text`,text:`suggestionActions: ${t}`}]}}function st(e,t,n){if(Ye(t))return n?ot(t,n):t;if(typeof t==`string`)return n?{content:[{type:`text`,text:JSON.stringify({result:t,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:t}]};if(t===void 0)return n?{content:[{type:`text`,text:JSON.stringify({result:`Custom tool "${e}" completed successfully`,suggestionActions:n},null,2)}]}:{content:[{type:`text`,text:`Custom tool "${e}" completed successfully`}]};let r=V(t)&&n?at(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var ct=class{constructor(t,n,r=new e.x,i){this.pageRegistry=t,this.extensionTaskQueue=n,this.telemetry=r,this.browserService=i}resolveToolPage(t,n,r){if(r.page)return r.page;if(r.mode===`extension`&&this.extensionTaskQueue){let t=new e.p(this.extensionTaskQueue);return t.setTarget(n,r.browserId),t}throw Error(`Custom tool "${t}" requires a supported page context`)}toPageSummary(e,t){return{pageId:t.id,url:t.url,title:t.title,active:e===t.id}}async createPageForBrowser(e,t,n){if(!this.browserService)throw Error(`Custom tool "${e}" requires browser service support to create a page`);let r=this.browserService.getBrowser(t);if(!r)throw Error(`Browser "${t}" not found`);let i=n?.setAsCurrent!==!1;if(r.mode===`extension`||r.mode===`vm`){if(!this.extensionTaskQueue)throw Error(`Custom tool "${e}" requires extension task support to create a page`);let a=this.pageRegistry.registerExtensionPage(t,void 0,n?.url,!1);try{let o=await this.extensionTaskQueue.queueTask(`browser_new_page`,{browserId:t,pageId:a,url:n?.url,setAsCurrent:i},1e4,t);if(!o.success)throw Error(o.error??`Custom tool "${e}" failed to create a page`);let s=o.result?.content[0]?.type===`text`?o.result.content[0].text:void 0,c={};if(typeof s==`string`&&s.length>0)try{c=JSON.parse(s)}catch{c={}}let l=this.pageRegistry.get(a);if(!l)throw Error(`Page "${a}" was not registered`);l.url=c.url??l.url,l.title=c.title??l.title,l.extensionTabId=c.tabId??l.extensionTabId;let u=await this.waitForResolvedPageMetadata(a);return r.pageIds.add(a),(i||!r.currentPageId)&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a),{...this.toPageSummary(r.currentPageId,u),page:this.resolveToolPage(e,a,u)}}catch(e){throw this.pageRegistry.remove(a),e}}let{pageId:a,page:o}=await this.browserService.newPage(t);n?.url&&(await o.goto(n.url),await this.pageRegistry.updateMetadata(a)),i&&this.browserService.setCurrentPage(t,a),this.browserService.recordBrowserActivity(t,a);let s=this.pageRegistry.get(a);if(!s)throw Error(`Page "${a}" was not registered`);return{...this.toPageSummary(r.currentPageId,s),page:this.resolveToolPage(e,a,s)}}async waitForResolvedPageMetadata(e){let t=Date.now(),n=this.pageRegistry.get(e);for(;n&&Date.now()-t<1500;){let t=typeof n.url==`string`&&n.url.length>0,r=typeof n.title==`string`&&n.title.length>0&&n.title!==`Extension Tab`;if(t&&r)return n;await new Promise(e=>setTimeout(e,50)),n=this.pageRegistry.get(e)}if(!n)throw Error(`Page "${e}" was not registered`);return n}async resolveExecutionContext(e,t){let n=typeof t[R]==`string`&&t[R].length>0?t[R]:void 0,r=typeof t[z]==`string`&&t[z].length>0?t[z]:void 0;if(!n&&!r)throw Error(`Custom tool "${e}" requires a string pageId or browserId`);if(n){let t=this.pageRegistry.get(n);if(!t)throw Error(`Page "${n}" not found`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}let i=this.createBrowserHelper(e,r);try{let e=await i.getCurrentPage(),t=this.pageRegistry.get(e.pageId);if(!t)throw Error(`Page "${e.pageId}" not found`);return{pageId:e.pageId,pageEntry:t,page:e.page,browser:i}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await i.newPage(),n=this.pageRegistry.get(t.pageId);if(!n)throw Error(`Page "${t.pageId}" not found`,{cause:e});return{pageId:t.pageId,pageEntry:n,page:t.page,browser:i}}}createBrowserHelper(e,t){return{browserId:t,mode:this.browserService?.getBrowser(t)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(t);if(!e)throw Error(`Browser "${t}" not found`);return this.pageRegistry.findByBrowser(t).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async n=>{let r=this.pageRegistry.get(n);if(!r||r.browserId!==t)throw Error(`Page "${n}" not found in browser "${t}"`);let i=this.browserService?.getBrowser(t);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(e,n,r)}},getCurrentPage:async()=>{let n=this.browserService?.getBrowser(t);if(!n)throw Error(`Browser "${t}" not found`);let r=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},newPage:async n=>this.createPageForBrowser(e,t,n)}}createToolLogger(e,t,n){let r={"browse_tool.tool.name":e,"browse_tool.page.id":t,"browse_tool.browser.id":n.browserId,"browse_tool.execution.mode":n.mode},i=(e,t,n)=>{this.telemetry.log(e,t,{attributes:Xe({...r,...n?.attributes??{}}),exception:n?.exception})};return{getTraceContext:()=>this.telemetry.getActiveTraceContext(),trace:(e,t)=>i(`trace`,e,t),debug:(e,t)=>i(`debug`,e,t),info:(e,t)=>i(`info`,e,t),warn:(e,t)=>i(`warn`,e,t),error:(e,t)=>i(`error`,e,t),fatal:(e,t)=>i(`fatal`,e,t)}}async listTools(e){return(await this.loadTools(e)).map(({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i})=>({name:e,description:t,suggestionActions:n,inputSchema:r,capabilities:i}))}async executeTool(e,t,n){let r=typeof n[R]==`string`?n[R]:void 0,i=typeof n[z]==`string`?n[z]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":l.default.resolve(e),"browse_tool.page.id":r,"browse_tool.browser.id":i}},async r=>{let i=(await this.loadTools(e)).find(e=>e.name===t);if(!i)throw r?.setStatus({code:p.SpanStatusCode.ERROR,message:`Custom tool "${t}" not found`}),Error(`Custom tool "${t}" not found`);let{pageId:a,pageEntry:o,page:s,browser:c}=await this.resolveExecutionContext(t,n),u=this.createToolLogger(t,a,o);r?.setAttributes({"browse_tool.browser.id":o.browserId,"browse_tool.execution.mode":o.mode,"browse_tool.page.id":a}),B(`Executing custom tool`,{toolName:t,directory:l.default.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:Je(n),scriptPath:i.scriptPath});let d;try{d=await i.execute?.({page:s,browser:c,input:n,logger:u})}catch(e){let n=e instanceof Error?e.message:String(e);throw B(`Custom tool execution failed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,error:n,stack:e instanceof Error?e.stack:void 0}),Error(`Custom tool "${t}" failed on page "${a}": ${n}`,{cause:e})}B(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:Je(d)});let f=st(t,d,i.suggestionActions),m=f.isError?f.content[0]?.text:void 0;return m&&r?.setStatus({code:p.SpanStatusCode.ERROR,message:m}),f})}async loadTools(e){let t=l.default.resolve(e),n=l.default.join(t,`tools.yaml`),r=tt(await(0,g.readFile)(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=We.parse(r);return Promise.all(i.tools.map(async e=>{nt(e);let n=l.default.resolve(t,e.script);await(0,g.stat)(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=it(n,await rt(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function lt(t){let n=new u.Hono;return n.get(`/browsers`,async n=>{try{let r=t.get(e.C.BrowserService),i=t.get(e.C.PageRegistry),a=r.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString(),pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt.toISOString()}))}}),c={totalBrowsers:a.length,totalPages:o.length};return n.json({browsers:s,stats:c})}catch(e){return console.error(`Failed to list browsers:`,e),n.json({error:`Failed to list browsers`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/browsers`,async n=>{try{return await t.get(e.C.BrowserService).closeAll(),n.json({success:!0,message:`All browsers closed`})}catch(e){return console.error(`Failed to close all browsers:`,e),n.json({error:`Failed to close all browsers`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/browsers/:id`,async n=>{try{let r=n.req.param(`id`),i=t.get(e.C.BrowserService);return i.getBrowser(r)?(await i.closeBrowser(r),n.json({success:!0,message:`Browser "${r}" closed`})):n.json({error:`Browser "${r}" not found`},404)}catch(e){return console.error(`Failed to close browser:`,e),n.json({error:`Failed to close browser`,message:e instanceof Error?e.message:String(e)},500)}}),n.delete(`/pages/:id`,async n=>{try{let r=n.req.param(`id`),i=t.get(e.C.PageRegistry).get(r);return i?i.page?(await i.page.close(),n.json({success:!0,message:`Page "${r}" closed`})):n.json({error:`Page "${r}" is in extension mode and cannot be closed via API`},400):n.json({error:`Page "${r}" not found`},404)}catch(e){return console.error(`Failed to close page:`,e),n.json({error:`Failed to close page`,message:e instanceof Error?e.message:String(e)},500)}}),n}function ut(e){return e.toLocaleString(`en-US`,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1})}function dt(e){let t=Math.floor(e/1e3),n=Math.floor(t/60),r=Math.floor(n/60);return r>0?`${r}h ${n%60}m`:n>0?`${n}m ${t%60}s`:`${t}s`}function ft(e){return dt(new Date().getTime()-e.getTime())}const pt=`dashboard-container`,mt=`dashboard-header`,ht=`btn`,gt=`btn-danger`,_t=`table-container`,vt=`browser-table`,yt=`stats-container`,bt=`stat-card`,xt=`browser-row`,St=`page-row`,W=`status-active`,Ct=`url-cell`,G=`timestamp-cell`,K=`actions-cell`,q=`kill-btn`,wt=`refresh-btn`,Tt=`empty-state`,Et=`
|
|
6
6
|
* {
|
|
7
7
|
margin: 0;
|
|
8
8
|
padding: 0;
|
|
@@ -243,7 +243,7 @@ const e=require(`./streamable-http-O6YYT3Yr.cjs`);require(`./playwright-test-Dfp
|
|
|
243
243
|
margin-bottom: 0.5rem;
|
|
244
244
|
color: #999;
|
|
245
245
|
}
|
|
246
|
-
`;function
|
|
246
|
+
`;function Dt({browsers:e}){return e.length===0?(0,b.jsx)(`div`,{class:_t,children:(0,b.jsxs)(`div`,{class:Tt,children:[(0,b.jsx)(`h3`,{children:`No Active Browsers`}),(0,b.jsx)(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):(0,b.jsx)(`div`,{class:_t,children:(0,b.jsxs)(`table`,{class:`browser-table`,children:[(0,b.jsx)(`thead`,{children:(0,b.jsxs)(`tr`,{children:[(0,b.jsx)(`th`,{children:`ID`}),(0,b.jsx)(`th`,{children:`Status`}),(0,b.jsx)(`th`,{children:`URL / Title`}),(0,b.jsx)(`th`,{children:`Created`}),(0,b.jsx)(`th`,{children:`Age`}),(0,b.jsx)(`th`,{children:`Actions`})]})}),(0,b.jsx)(`tbody`,{id:`browser-table-body`,children:e.map(e=>(0,b.jsxs)(b.Fragment,{children:[(0,b.jsxs)(`tr`,{class:xt,children:[(0,b.jsx)(`td`,{children:e.id}),(0,b.jsx)(`td`,{children:(0,b.jsxs)(`span`,{class:W,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),(0,b.jsx)(`td`,{children:e.profileName||`Default Profile`}),(0,b.jsx)(`td`,{class:G,children:ut(e.createdAt)}),(0,b.jsx)(`td`,{class:G,children:ft(e.createdAt)}),(0,b.jsx)(`td`,{class:K,children:(0,b.jsx)(`button`,{class:q,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>(0,b.jsxs)(`tr`,{class:St,children:[(0,b.jsxs)(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),(0,b.jsx)(`td`,{children:(0,b.jsx)(`span`,{class:W,children:`Open`})}),(0,b.jsx)(`td`,{class:Ct,title:t.url,children:t.title||t.url||`about:blank`}),(0,b.jsx)(`td`,{class:G,children:ut(t.createdAt)}),(0,b.jsx)(`td`,{class:G,children:ft(t.createdAt)}),(0,b.jsx)(`td`,{class:K,children:(0,b.jsx)(`button`,{class:q,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]}))]}))})]})})}function Ot({stats:e}){return(0,b.jsxs)(`div`,{class:`stats-container`,children:[(0,b.jsxs)(`div`,{class:bt,children:[(0,b.jsx)(`h3`,{children:`Active Browsers`}),(0,b.jsx)(`div`,{class:`value`,children:e.totalBrowsers})]}),(0,b.jsxs)(`div`,{class:bt,children:[(0,b.jsx)(`h3`,{children:`Active Pages`}),(0,b.jsx)(`div`,{class:`value`,children:e.totalPages})]})]})}function kt({browsers:e,stats:t}){return(0,b.jsxs)(`div`,{class:`dashboard-container`,children:[(0,b.jsxs)(`div`,{class:`dashboard-header`,children:[(0,b.jsxs)(`div`,{children:[(0,b.jsx)(`h1`,{children:`Playwright MCP Dashboard`}),(0,b.jsx)(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),(0,b.jsxs)(`div`,{children:[(0,b.jsx)(`button`,{id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),(0,b.jsx)(`button`,{id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),(0,b.jsx)(Ot,{stats:t}),(0,b.jsx)(Dt,{browsers:e}),(0,b.jsx)(`script`,{children:(0,y.raw)(`
|
|
247
247
|
class DashboardManager {
|
|
248
248
|
constructor() {
|
|
249
249
|
this.autoRefreshInterval = null;
|
|
@@ -281,7 +281,7 @@ class DashboardManager {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
updateStats(stats) {
|
|
284
|
-
const cards = document.querySelectorAll('.${
|
|
284
|
+
const cards = document.querySelectorAll('.${bt} .value');
|
|
285
285
|
if (cards.length >= 2) {
|
|
286
286
|
cards[0].textContent = stats.totalBrowsers;
|
|
287
287
|
cards[1].textContent = stats.totalPages;
|
|
@@ -293,10 +293,10 @@ class DashboardManager {
|
|
|
293
293
|
if (!tbody) return;
|
|
294
294
|
|
|
295
295
|
if (browsers.length === 0) {
|
|
296
|
-
const container = tbody.closest('.${
|
|
296
|
+
const container = tbody.closest('.${_t}');
|
|
297
297
|
if (container) {
|
|
298
298
|
container.innerHTML = \`
|
|
299
|
-
<div class="${
|
|
299
|
+
<div class="${Tt}">
|
|
300
300
|
<h3>No Active Browsers</h3>
|
|
301
301
|
<p>Launch a browser using MCP tools to see it here.</p>
|
|
302
302
|
</div>
|
|
@@ -310,7 +310,7 @@ class DashboardManager {
|
|
|
310
310
|
browsers.forEach(browser => {
|
|
311
311
|
// Browser row
|
|
312
312
|
const browserRow = document.createElement('tr');
|
|
313
|
-
browserRow.className = '${
|
|
313
|
+
browserRow.className = '${xt}';
|
|
314
314
|
browserRow.innerHTML = \`
|
|
315
315
|
<td>\${browser.id}</td>
|
|
316
316
|
<td><span class="${W}">\${browser.pages.length} page\${browser.pages.length !== 1 ? 's' : ''}</span></td>
|
|
@@ -324,12 +324,12 @@ class DashboardManager {
|
|
|
324
324
|
// Page rows
|
|
325
325
|
browser.pages.forEach(page => {
|
|
326
326
|
const pageRow = document.createElement('tr');
|
|
327
|
-
pageRow.className = '${
|
|
327
|
+
pageRow.className = '${St}';
|
|
328
328
|
const isActive = browser.currentPageId === page.id;
|
|
329
329
|
pageRow.innerHTML = \`
|
|
330
330
|
<td>\${page.id}\${isActive ? ' (active)' : ''}</td>
|
|
331
331
|
<td><span class="${W}">Open</span></td>
|
|
332
|
-
<td class="${
|
|
332
|
+
<td class="${Ct}" title="\${this.escapeHtml(page.url)}">\${this.escapeHtml(page.title || page.url || 'about:blank')}</td>
|
|
333
333
|
<td class="${G}">\${this.formatTimestamp(page.createdAt)}</td>
|
|
334
334
|
<td class="${G}">\${this.formatAge(page.createdAt)}</td>
|
|
335
335
|
<td class="${K}"><button class="${q}" onclick="dashboard.killPage('\${page.id}')">Close</button></td>
|
|
@@ -428,7 +428,7 @@ const dashboard = new DashboardManager();
|
|
|
428
428
|
window.addEventListener('beforeunload', () => {
|
|
429
429
|
dashboard.stopAutoRefresh();
|
|
430
430
|
});
|
|
431
|
-
`)})]})}function
|
|
431
|
+
`)})]})}function At({title:e,children:t}){return(0,b.jsxs)(`html`,{lang:`en`,children:[(0,b.jsxs)(`head`,{children:[(0,b.jsx)(`meta`,{charset:`UTF-8`}),(0,b.jsx)(`meta`,{name:`viewport`,content:`width=device-width, initial-scale=1.0`}),(0,b.jsx)(`title`,{children:e}),(0,b.jsx)(`style`,{children:(0,y.raw)(`
|
|
432
432
|
* {
|
|
433
433
|
margin: 0;
|
|
434
434
|
padding: 0;
|
|
@@ -669,8 +669,8 @@ window.addEventListener('beforeunload', () => {
|
|
|
669
669
|
margin-bottom: 0.5rem;
|
|
670
670
|
color: #999;
|
|
671
671
|
}
|
|
672
|
-
`)})]}),(0,y.jsx)(`body`,{children:t})]})}function vt(t){let n=new o.Hono;return n.get(`/`,async n=>{try{let r=t.get(e.C.BrowserService),i=t.get(e.C.PageRegistry),a=r.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt,pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt}))}}),c={totalBrowsers:a.length,totalPages:o.length};return n.html((0,y.jsx)(_t,{title:`Playwright MCP Dashboard`,children:(0,y.jsx)(gt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html((0,y.jsx)(_t,{title:`Playwright MCP Dashboard - Error`,children:(0,y.jsxs)(`div`,{style:`padding: 2rem; text-align: center;`,children:[(0,y.jsx)(`h1`,{children:`Failed to load dashboard`}),(0,y.jsx)(`p`,{children:e instanceof Error?e.message:String(e)}),(0,y.jsx)(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function yt(e){if(!e.isError)return;let t=e.content[0];return t?.type===`text`&&typeof t.text==`string`?t.text:`Unknown tool execution error`}function bt(t){try{return t.get(e.C.TelemetryService)}catch{return new e.x}}function xt(t){let n=new o.Hono,r=t.get(e.C.PageRegistry),i=t.get(e.C.BrowserService),a=t.get(e.C.ExtensionTaskQueue),c=bt(t),l=new Xe(r,a,c,i);n.use(`*`,(0,s.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`DELETE`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`,`Authorization`]})),n.get(`/health`,e=>{try{let t=i.listBrowsers(),n={status:`healthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,timestamp:new Date().toISOString(),browsers:{count:t.length,instances:t.map(e=>({id:e.id,pageCount:e.pageIds.size,createdAt:e.createdAt.toISOString()}))}};return e.json(n)}catch(t){return e.json({status:`unhealthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,error:t instanceof Error?t.message:String(t)},503)}}),n.post(`/execute`,async n=>{try{let i=await n.req.json();if(!i.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let a=typeof i.arguments?.pageId==`string`?i.arguments.pageId:void 0;return await c.runInSpan(`browse_tool.http.execute`,{attributes:{"http.method":`POST`,"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":a}},async a=>{let o=t.getAll(e.C.Tool).find(e=>e.getDefinition().name===i.tool);if(process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`&&i.tool===`browser_launch`){let e=typeof i.arguments?.videoDir==`string`?i.arguments.videoDir:typeof i.arguments?.video_dir==`string`?i.arguments.video_dir:``;console.log(`[ExtensionRecordingDebug] http execute browser_launch mode=${String(i.arguments?.mode??``)} videoDir=${e}`)}if(!o)return a?.setStatus({code:d.SpanStatusCode.ERROR,message:`Tool "${i.tool}" not found`}),c.log(`warn`,`browse-tool HTTP execute rejected unknown tool`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool}}),n.json({success:!1,error:`Tool "${i.tool}" not found`},404);let s=await o.execute(i.arguments||{}),l=i.arguments||{},u=typeof l.pageId==`string`?l.pageId:void 0;if(u){let n=t.get(e.C.BrowserService),i=r.get(u);i&&n.recordBrowserActivity(i.browserId,u)}let f=yt(s);return f?(a?.setStatus({code:d.SpanStatusCode.ERROR,message:f}),c.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}})):c.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}}),n.json({success:!s.isError,result:s,error:f})})}catch(e){return c.log(`error`,`browse-tool HTTP execute request crashed`,{attributes:{"http.route":`/execute`},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/browsers`,n=>{try{let r=t.get(e.C.BrowserService).listBrowsers();return n.json({browsers:r.map(e=>({id:e.id,profileName:e.profileName,pageIds:Array.from(e.pageIds),currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tools`,n=>{try{let r=t.getAll(e.C.Tool);return n.json({tools:r.map(e=>e.getDefinition())})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/custom-tools`,async e=>{let t=e.req.query(`dir`);if(!t)return e.json({tools:[],error:`Missing "dir" query parameter`},400);try{let n=await l.listTools(t);return e.json({tools:n})}catch(t){return e.json({tools:[],error:t instanceof Error?t.message:String(t)},400)}}),n.post(`/custom-tools`,async n=>{let i=n.req.query(`dir`);if(!i)return n.json({success:!1,error:`Missing "dir" query parameter`},400);try{let a=await n.req.json();if(!a.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let o=typeof a.arguments?.pageId==`string`?a.arguments.pageId:void 0;return await c.runInSpan(`browse_tool.http.custom_tool.execute`,{attributes:{"http.method":`POST`,"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":o,"browse_tool.custom_tools.directory":i}},async s=>{let u=await l.executeTool(i,a.tool,a.arguments||{});if(o){let n=t.get(e.C.BrowserService),i=r.get(o);i&&n.recordBrowserActivity(i.browserId,o)}let f=yt(u);return f?(s?.setStatus({code:d.SpanStatusCode.ERROR,message:f}),c.log(`error`,`browse-tool custom tool execution failed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":o}})):c.log(`info`,`browse-tool custom tool execution succeeded`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":o}}),n.json({success:!u.isError,result:u,error:f})})}catch(e){return c.log(`error`,`browse-tool custom tool request crashed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.custom_tools.directory":i},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let u=C(t);n.route(`/extension`,u);let f=Ze(t);n.route(`/api`,f);let p=vt(t);return n.route(`/`,p),n}function St(t,n,r){t.get(`/ws/extension/:browserId`,r(t=>{let r=t.req.param(`browserId`),i=null;return{onOpen(t,a){try{i=n.get(e.C.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.C.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.C.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.C.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.C.WebSocketHub).getStats();return t.json({totalConnections:r.totalConnections,browserCount:r.browserCount,connections:r.connections.map(e=>({id:e.id,browserId:e.browserId,connectedAt:e.connectedAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}})}const Ct=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function wt(e=process.cwd()){let t=u.default.resolve(e);for(;;){for(let e of Ct)if((0,l.existsSync)(u.default.join(t,e)))return t;let e=u.default.dirname(t);if(e===t)return process.cwd();t=e}}function Tt(e,t){let n=u.default.resolve(e),r=u.default.extname(n)===`.json`?u.default.dirname(n):n;return u.default.join(r,t)}async function Et(e,t){let n=await e.registerProcess({...t,force:!0});if(!n.success||!n.record)throw Error(n.error||`Failed to register process ${t.serviceName}`);let r=!1;return{release:async()=>{if(r)return;r=!0;let n=await e.releaseProcess({repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:t.serviceType,environment:t.environment,pid:t.pid,kill:!1,releasePort:!1});!n.success&&!n.error?.includes(`No matching process entry`)&&console.error(`Failed to release process registry entry for ${t.serviceName}: ${n.error}`)}}}const Dt=new t.Command(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(e.l)).option(`--host <host>`,`Host to bind`,e.d()).option(`--headless`,`Run browsers in headless mode by default`,!0).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before closing browsers`,`30`).option(`--registry-dir <path>`,`Custom registry path or directory for service discovery`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking (deprecated)`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--snippets-dir <path>`,`Directory used by browser_run_code to save and load reusable snippets`).action(async function(t){let r=j(`httpServe`),i={port:M(this,`port`,t.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),headless:M(this,`headless`,t.headless,r.headless),idleTimeout:M(this,`idleTimeout`,t.idleTimeout,r.idleTimeout===void 0?void 0:String(r.idleTimeout),process.env[e.c]),host:M(this,`host`,t.host,r.host,process.env.PLAYWRIGHT_HOST),registryDir:M(this,`registryDir`,t.registryDir,r.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:M(this,`registryPath`,t.registryPath,r.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:M(this,`pidsDir`,t.pidsDir,r.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:M(this,`profilesDir`,t.profilesDir,r.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:M(this,`snippetsDir`,t.snippetsDir,r.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR)},a;try{let t=Number.parseInt(i.port,10),r=`browse-tool-http`,o=process.env.NODE_ENV||`development`,s=wt(process.cwd()),c=i.registryPath||i.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;c&&(process.env.PLAYWRIGHT_REGISTRY_DIR=c,process.env.PORT_REGISTRY_PATH=c,process.env.PROCESS_REGISTRY_PATH=Tt(c,`processes.json`)),process.env.PLAYWRIGHT_HOST=i.host,process.env.PLAYWRIGHT_PORT=i.port,process.env[e.c]=i.idleTimeout,i.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=i.pidsDir),i.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=i.profilesDir),i.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=u.default.resolve(i.snippetsDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${t}`),console.log(` Host: ${i.host}`),console.log(` Headless: ${i.headless}`),console.log(` Idle Timeout: ${i.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`);let l=e.a(),d=l.get(e.C.ProcessRegistryService),p=xt(l),{injectWebSocket:m,upgradeWebSocket:h}=(0,_.createNodeWebSocket)({app:p});St(p,l,h);let g=l.get(e.C.WebSocketHub),v=l.get(e.C.ExtensionTaskQueue),y=l.get(e.C.BrowserService),b=l.get(e.C.ExtensionSessionRegistry),x=l.get(e.C.PageRegistry),S=l.get(e.C.IdleCleanupService);S.start(),g.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=y.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=x.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,g.reassignConnection(e.id,i),a||(a=x.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,y.registerExtensionBrowserWithId(i),a=x.registerExtensionPage(i);let e=y.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=x.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=b.register({browserId:i,tabId:t.payload.tabId,url:t.payload.url,metadata:{transport:`websocket`}});g.sendSessionAck(e,t.id??``,s.id,s.controlMode),g.broadcastPageCreated(i,a,x.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=v.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&y.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=x.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,y.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&b.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let C=new f.PortRegistryService(process.env.PORT_REGISTRY_PATH);c?console.log(` Registry path: ${c}`):console.log(` Registry path: default (~/.port-registry/ports.json)`);let w=await C.reservePort({repositoryPath:s,serviceName:r,serviceType:`tool`,environment:o,preferredPort:t,pid:process.pid,host:i.host,force:!0,portRange:{min:t,max:t},metadata:{healthCheckUrl:`${e.u(i.host,t)}/health`,headless:i.headless,idleTimeout:i.idleTimeout}});if(!w.success||!w.record)throw Error(w.error||`Failed to reserve port ${t} in global registry`);let T=t,E;try{E=(0,n.serve)({fetch:p.fetch,port:T,hostname:i.host})}catch(e){throw await C.releasePort({repositoryPath:s,serviceName:r,serviceType:`tool`,environment:o,pid:process.pid}),e}m(E),a=await Et(d,{repositoryPath:s,serviceName:r,serviceType:`service`,environment:o,pid:process.pid,port:T,host:i.host,metadata:{healthCheckUrl:`${e.u(i.host,T)}/health`,headless:i.headless,idleTimeout:i.idleTimeout,transport:`http`}});let D=e.u(i.host,T);console.log(`HTTP server listening on ${D}`),console.log(` Health check: ${D}/health`),console.log(` Execute tool: POST ${D}/execute`),console.log(`
|
|
673
|
-
Press Ctrl+C to stop the server`);let O=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{S.stop(),E.close(),console.log(`Server closed`);try{await l.get(e.C.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{a&&await a.release()}catch(e){console.error(`Process registry cleanup error:`,e)}try{await C.releasePort({repositoryPath:s,serviceName:r,serviceType:`tool`,environment:o,pid:process.pid}),console.log(`Service deregistered`)}catch{}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>O(`SIGINT`)),process.on(`SIGTERM`,()=>O(`SIGTERM`))}catch(e){if(a)try{await a.release()}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Ot={browser_click:[`input`],browser_fill:[`input`],browser_type:[`input`],browser_select:[`input`],browser_hover:[`input`],browser_drag:[`input`],browser_press_key:[`input`],browser_upload_file:[`input`],browser_navigate:[`navigation`],browser_go_back:[`navigation`],browser_go_forward:[`navigation`],browser_reload:[`navigation`],browser_wait_for:[`navigation`],browser_snapshot:[`snapshot`],browser_screenshot:[`snapshot`],browser_pdf:[`snapshot`],browser_list_pages:[`page`],browser_new_page:[`page`],browser_select_page:[`page`],browser_close_page:[`page`],browser_resize_page:[`page`,`emulation`],browser_handle_dialog:[`dialog`],browser_list_network_requests:[`network`],browser_get_network_request:[`network`],browser_list_console_messages:[`console`],browser_evaluate_script:[`script`],browser_emulate:[`emulation`],browser_expect:[`testing`],browser_start_trace:[`tracing`],browser_stop_trace:[`tracing`],browser_list_profiles:[`profile`],browser_delete_profile:[`profile`],run_spec:[`spec`,`testing`],discover_specs:[`spec`,`testing`],browser_launch:[`browser`],browser_close:[`browser`],browser_run_code:[`code`,`script`]};function kt(e){let t=new Set;for(let[n,r]of Object.entries(Ot))r.some(t=>e.includes(t))&&t.add(n);return t}const At=3e3,jt=4,Mt=150,Nt=[`browser_launch`],Pt=[`browser_new_page`];function Ft(e){if(!e?.content?.[0]?.text)return null;try{let t=JSON.parse(e.content[0].text);return{browserId:t.browserId,pageId:t.pageId,mode:t.mode,url:t.url}}catch{return null}}async function It(e){await new Promise(t=>setTimeout(t,e))}async function Lt(e){let t=new AbortController,n=setTimeout(()=>t.abort(),At);try{let n=await fetch(`${e}/tools`,{signal:t.signal});if(!n.ok)throw Error(`HTTP error ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);if(!Array.isArray(r.tools))throw Error(`Invalid /tools response: missing tools array`);if(r.tools.length===0)throw Error(`HTTP server returned an empty tool list`);return r.tools}finally{clearTimeout(n)}}function Rt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function zt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),At);try{let r=await fetch(Rt(e,t),{signal:n.signal});if(!r.ok)throw Error(`HTTP error ${r.status}: ${r.statusText}`);let i=await r.json();if(i.error)throw Error(i.error);if(!Array.isArray(i.tools))throw Error(`Invalid /custom-tools response: missing tools array`);return i.tools}finally{clearTimeout(r)}}async function Bt(e){let t;for(let n=1;n<=4;n+=1)try{return await Lt(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await It(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function Vt(e,t){let n;for(let r=1;r<=4;r+=1)try{return await zt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await It(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function Ht(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?kt(e.tags):null,n=new Set(e.exclude??[]);if(!t)return n.size>0?n:null;for(let e of n)t.delete(e);return t}function Ut(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:i,toolFilter:o,customToolsDir:s,enforcedProfileName:c}=e,l=o?.tags?.length?Ht(o):null,u=new Set(o?.exclude??[]),d=l!==null||u.size>0,f=new r.Server({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),p={name:`browser_list_session`,description:`List all browsers and pages created in this MCP session. Use this to see what resources you have available.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}},m=[],h=[],g=new Set;function _(e){return d?e.filter(e=>u.has(e.name)?!1:l===null?!0:l.has(e.name)):e}function v(e){return e.filter(e=>!u.has(e.name))}function y(e){return c?{...e,profileName:c}:e}function b(e){if(!c)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===c);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function x(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function S(e){return u.has(e)?!1:l===null?!0:l.has(e)}return f.setRequestHandler(a.ListToolsRequestSchema,async()=>{try{let e=await Bt(t);m=e,g=new Set(e.map(e=>e.name));let r=s?await Vt(t,s):[];h=r;let i=[..._(e),...v(r).map(e=>x(e))];return n&&i.push(p),{tools:i}}catch(e){if(m.length>0||h.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[..._(m),...v(h).map(e=>x(e))];return n&&t.push(p),{tools:t}}let r=e instanceof Error?e.message:String(e);throw Error(`Failed to list tools from HTTP server at ${t}: ${r}`,{cause:e})}}),f.setRequestHandler(a.CallToolRequestSchema,async e=>{let{name:r,arguments:a}=e.params,o=new Set(h.map(e=>e.name));if(r!==`browser_list_session`&&!S(r))return{content:[{type:`text`,text:`Tool "${r}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(r===`browser_list_session`&&n){let e=n.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let c=a||{};r===`browser_launch`&&i&&(c={...c,mode:i}),r===`browser_launch`&&(c=y(c));try{if(s&&!o.has(r)&&!g.has(r))try{h=await Vt(t,s),o=new Set(h.map(e=>e.name))}catch(e){console.error(`Failed to refresh custom tools, using cached list: ${e instanceof Error?e.message:String(e)}`)}let e=o.has(r),i=await fetch(e&&s?Rt(t,s):`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:r,arguments:c})});if(!i.ok){let e=await i.text();return{content:[{type:`text`,text:`HTTP error ${i.status}: ${e}`}],isError:!0}}let a=await i.json();if(!a.success)return{content:[{type:`text`,text:a.error||a.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(n&&a.result){if(Nt.includes(r)){let e=Ft(a.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(Pt.includes(r)){let e=Ft(a.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}}return r===`browser_list_profiles`&&a.result?b(a.result):a.result||{content:[{type:`text`,text:`No result returned`}]}}catch(e){return{content:[{type:`text`,text:`Failed to execute tool via HTTP server: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),f}const J=`stdio`,Wt=`http`,Y=`streamable-http`,Gt=new Set([J,Wt,Y]),Kt=`chromium`,qt=new Set([Kt,`firefox`,`webkit`]),Jt=new Set([`playwright`,`extension`,`vm`,`stealth`]),Yt=`browser_close`,Xt=`,`,Zt=`spawned`,Qt=`reused`,X=`SIGINT`,Z=`SIGTERM`,$t=`PLAYWRIGHT_REGISTRY_DIR`,en=`PORT_REGISTRY_PATH`,tn=`PLAYWRIGHT_PIDS_DIR`,nn=`PLAYWRIGHT_PROFILES_DIR`,rn=`PLAYWRIGHT_HOST`,an=`PLAYWRIGHT_PORT`,on=`PLAYWRIGHT_MCP_PORT`,sn=`x-profile`,cn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Q=`browse-tool-mcp-http`,ln=`browse-tool-mcp`,un=`tool`,dn=`service`,fn=0,pn=1,mn=3201;var hn=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...Gt].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},gn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...qt].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},_n=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...Jt].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function vn(e){let t=e.toLowerCase();if(!qt.has(t))throw new gn(e);return t}function yn(e){let t=e.toLowerCase();if(!Jt.has(t))throw new _n(e);return t}function bn(e){let t=typeof e==`number`?e:Number.parseInt(e,10);if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}function xn(e){let t=e.toLowerCase();return t===Wt?Y:t}function Sn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function Cn(e=process.cwd()){let t=u.default.resolve(e);for(;;){for(let e of cn)if((0,l.existsSync)(u.default.join(t,e)))return t;let e=u.default.dirname(t);if(e===t)return process.cwd();t=e}}function wn(e,t){let n=u.default.resolve(e),r=u.default.extname(n)===`.json`?u.default.dirname(n):n;return u.default.join(r,t)}async function Tn(e,t){let n=await e.registerProcess({...t,force:!0});if(!n.success||!n.record)throw Error(n.error||`Failed to register process ${t.serviceName}`);let r=!1;return{release:async()=>{if(r)return;r=!0;let n=await e.releaseProcess({repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:t.serviceType,environment:t.environment,pid:t.pid,kill:!1,releasePort:!1});!n.success&&!n.error?.includes(`No matching process entry`)&&console.error(`Failed to release process registry entry for ${t.serviceName}: ${n.error}`)}}}function En(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function Dn(e){let t=e.tags?En(e.tags):void 0,n=e.exclude?En(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var On=class extends Error{code=`BROWSER_CLEANUP_FAILED`;recovery=`Browsers may still be running; check the HTTP server or kill processes manually`;failedBrowserIds;httpBaseUrl;constructor(e,t,n){super(`Failed to close ${e.length} browser(s): ${e.join(`, `)}`,n),this.name=`BrowserCleanupError`,this.failedBrowserIds=e,this.httpBaseUrl=t}},kn=class extends Error{code=`SESSION_SHUTDOWN_FAILED`;recovery=`Check logs for cleanup and transport errors; processes may need manual cleanup`;signal;constructor(e,t){super(`Shutdown failed for signal ${e}`,t),this.name=`SessionShutdownError`,this.signal=e}},An=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},jn=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function Mn(e,t){let n=e.getBrowserIds(),r=[],i;for(let e of n)try{let n=await fetch(`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!n.ok){let e=await n.text().catch(()=>``);throw Error(`HTTP ${n.status} from ${t}/execute: ${e}`)}console.error(` Closed browser: ${e}`)}catch(t){let n=t instanceof Error?t:Error(String(t));console.error(` Failed to close browser: ${e}`,n),r.push(e),i??=n}if(r.length>0)throw new On(r,t,{cause:i});e.clear()}async function Nn(e,t,n,r){await e.start();let i=!1,a=async a=>{if(i)return;i=!0,console.error(`\nReceived ${a}, shutting down gracefully...`);let o;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await Mn(t,n))}catch(e){o=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,o)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),o??=t}try{await r?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),o??=t}if(o){let e=new kn(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(X,()=>a(X)),process.once(Z,()=>a(Z))}const Pn=new t.Command(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[J,Y].join(`, `)}`,J).option(`-b, --browser <browser>`,`Default browser type: ${[...qt].join(`, `)}`,Kt).option(`--headless`,`Run browsers in headless mode by default`,!1).option(`--no-headless`,`Run browsers in headed mode (visible window)`).option(`--host <host>`,`Default host for HTTP services`,e.d()).option(`--port <port>`,`Port for streamable HTTP transport`,`3201`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...Jt].join(`, `)}`).option(`--tags <tags>`,`Comma-separated tags to filter tools (e.g. input,navigation,snapshot)`).option(`--exclude <tools>`,`Comma-separated tool names to exclude (applied after --tags)`).option(`--custom-tools <path>`,`Path to a folder containing tools.yaml and custom tool scripts`).option(`--snippets-dir <path>`,`Path to a folder where browser_run_code snippets are stored`).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before auto-closing browsers`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--registry-dir <path>`,`Custom registry directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).action(async function(t){let n=j(`mcpServe`),r={type:M(this,`type`,t.type,n.type),browser:M(this,`browser`,t.browser,n.browser),headless:M(this,`headless`,t.headless,n.headless),profile:M(this,`profile`,t.profile,n.profile),mode:M(this,`mode`,t.mode,n.mode),host:M(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),port:M(this,`port`,t.port,n.port,process.env.PLAYWRIGHT_MCP_PORT),tags:M(this,`tags`,t.tags,n.tags),exclude:M(this,`exclude`,t.exclude,n.exclude),customTools:M(this,`customTools`,t.customTools,n.customTools),snippetsDir:M(this,`snippetsDir`,t.snippetsDir,n.snippetsDir),idleTimeout:M(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[e.c]),registryPath:M(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:M(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:M(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:M(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR)},i=xn(r.type),a=Cn(process.cwd()),o=process.env.NODE_ENV||`development`,s;try{if(!Gt.has(i))throw new hn(i);let t=vn(r.browser),n=r.mode?yn(r.mode):void 0,c=bn(r.port??3201),l=Dn(r),d=r.customTools?u.default.resolve(r.customTools):void 0,p=r.snippetsDir?u.default.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${i}`),i===Y&&console.error(` MCP endpoint: http://${r.host??e.d()}:${c}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${r.headless}`),n&&console.error(` Default mode: ${n}`),r.profile&&console.error(` Profile: ${r.profile}`),l?.tags?.length&&console.error(` Tags: ${l.tags.join(`, `)}`),l?.exclude?.length&&console.error(` Exclude: ${l.exclude.join(`, `)}`),d&&console.error(` Custom tools: ${d}`),p&&console.error(` Snippets dir: ${p}`),r.idleTimeout&&console.error(` Idle timeout: ${r.idleTimeout} minutes`);let m=r.registryPath||r.registryDir;m&&(process.env.PLAYWRIGHT_REGISTRY_DIR=m,process.env.PORT_REGISTRY_PATH=m,process.env.PROCESS_REGISTRY_PATH=wn(m,`processes.json`),console.error(` Registry path: ${m}`)),r.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=r.pidsDir,console.error(` PIDs dir: ${r.pidsDir}`)),r.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=r.profilesDir,console.error(` Profiles dir: ${r.profilesDir}`)),p&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=p),r.idleTimeout&&(process.env[e.c]=r.idleTimeout),r.host&&(process.env.PLAYWRIGHT_HOST=r.host),process.env.PLAYWRIGHT_PORT=e.f().toString();let h=e.o(),g=h.get(e.C.ProcessRegistryService),_=await h.get(e.C.HttpServerManager).ensureRunning();if(!_.running)throw new An;let v=e.u(e.d(),_.port),y=_.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${y} on port ${_.port}`),i===J){let i=new e.s,c=new e.n(Ut({httpBaseUrl:v,sessionTracker:i,defaultMode:n,toolFilter:l,customToolsDir:d,enforcedProfileName:r.profile}));s=await Tn(g,{repositoryPath:a,serviceName:`browse-tool-mcp`,serviceType:dn,environment:o,pid:process.pid,metadata:{transport:J,browser:t,mode:n}}),await Nn(c,i,v,async()=>{await s?.release()});return}let b=new f.PortRegistryService(process.env.PORT_REGISTRY_PATH),x=await b.reservePort({repositoryPath:a,serviceName:Q,serviceType:un,environment:o,preferredPort:c,pid:process.pid,host:r.host??e.d(),force:!0,portRange:{min:c,max:c},metadata:{healthCheckUrl:`${e.u(r.host??e.d(),c)}/health`,transport:Y}});if(!x.success||!x.record)throw Error(x.error||`Failed to reserve ${Y} port ${c}`);let S=x.record.port;s=await Tn(g,{repositoryPath:a,serviceName:Q,serviceType:dn,environment:o,pid:process.pid,port:S,host:r.host??e.d(),metadata:{healthCheckUrl:`${e.u(r.host??e.d(),S)}/health`,transport:Y}});let C=new e.t(({headers:t})=>{let i=Sn(t,`x-profile`)??r.profile,a=new e.s;return{server:Ut({httpBaseUrl:v,sessionTracker:a,defaultMode:n,toolFilter:l,customToolsDir:d,enforcedProfileName:i}),onClose:async()=>{a.getSessionState().totalBrowsers>0&&await Mn(a,v)}}},{host:r.host??e.d(),port:S});try{await C.start()}catch(e){try{await s.release()}catch(e){console.error(`Process registry cleanup error:`,e)}throw await b.releasePort({repositoryPath:a,serviceName:Q,serviceType:un,environment:o,pid:process.pid}),e}let w=!1,T=async e=>{if(!w){w=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await C.stop();try{await s?.release()}catch(e){console.error(`Process registry cleanup error:`,e)}await b.releasePort({repositoryPath:a,serviceName:Q,serviceType:un,environment:o,pid:process.pid}),process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(X,()=>T(X)),process.once(Z,()=>T(Z))}catch(e){if(s)try{await s.release()}catch(e){console.error(`Process registry cleanup error:`,e)}(e instanceof hn||e instanceof gn||e instanceof _n||e instanceof An)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new jn({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),Fn=new t.Command(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=j(`httpServe`),n=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??t.registryPath??t.registryDir;n&&(process.env.PLAYWRIGHT_REGISTRY_DIR=n,process.env.PLAYWRIGHT_REGISTRY_PATH=n,process.env.PORT_REGISTRY_PATH=n),!process.env.PLAYWRIGHT_PIDS_DIR&&t.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=t.pidsDir),console.log(`browse-tool Status
|
|
674
|
-
`),console.log(`-`.repeat(50));let r=await e.i().get(e.C.HttpServerManager).getStatus();if(r.running){let n=process.env.PLAYWRIGHT_HOST??t.host??e.d();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${e.u(n,r.port)}/health`),r.browserCount!==void 0&&console.log(` Browsers: ${r.browserCount}`)}else console.log(`HTTP Server: Not Running`),r.error&&console.log(` Error: ${r.error}`);console.log(``),console.log(`-`.repeat(50)),process.exit(0)}catch(e){console.error(`Error getting status:`,e),process.exit(1)}}),
|
|
675
|
-
`)}}const
|
|
672
|
+
`)})]}),(0,b.jsx)(`body`,{children:t})]})}function jt(t){let n=new u.Hono;return n.get(`/`,async n=>{try{let r=t.get(e.C.BrowserService),i=t.get(e.C.PageRegistry),a=r.listBrowsers(),o=i.list(),s=a.map(e=>{let t=o.filter(t=>t.browserId===e.id);return{id:e.id,profileName:e.profileName,currentPageId:e.currentPageId,createdAt:e.createdAt,pages:t.map(e=>({id:e.id,url:e.url,title:e.title,createdAt:e.createdAt}))}}),c={totalBrowsers:a.length,totalPages:o.length};return n.html((0,b.jsx)(At,{title:`Playwright MCP Dashboard`,children:(0,b.jsx)(kt,{browsers:s,stats:c})}))}catch(e){return console.error(`Failed to render dashboard:`,e),n.html((0,b.jsx)(At,{title:`Playwright MCP Dashboard - Error`,children:(0,b.jsxs)(`div`,{style:`padding: 2rem; text-align: center;`,children:[(0,b.jsx)(`h1`,{children:`Failed to load dashboard`}),(0,b.jsx)(`p`,{children:e instanceof Error?e.message:String(e)}),(0,b.jsx)(`a`,{href:`/`,style:`color: #2196f3; text-decoration: underline;`,children:`Retry`})]})}),500)}}),n}function Mt(e){if(!e.isError)return;let t=e.content[0];return t?.type===`text`&&typeof t.text==`string`?t.text:`Unknown tool execution error`}function Nt(t){try{return t.get(e.C.TelemetryService)}catch{return new e.x}}function Pt(t){let n=new u.Hono,r=t.get(e.C.PageRegistry),i=t.get(e.C.BrowserService),a=t.get(e.C.ExtensionTaskQueue),o=Nt(t),s=new ct(r,a,o,i);n.use(`*`,(0,d.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`DELETE`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`,`Authorization`]})),n.get(`/health`,e=>{try{let t=i.listBrowsers(),n={status:`healthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,timestamp:new Date().toISOString(),browsers:{count:t.length,instances:t.map(e=>({id:e.id,pageCount:e.pageIds.size,createdAt:e.createdAt.toISOString()}))}};return e.json(n)}catch(t){return e.json({status:`unhealthy`,service:`browse-tool-http`,serviceName:`browse-tool-http`,error:t instanceof Error?t.message:String(t)},503)}}),n.post(`/execute`,async n=>{try{let i=await n.req.json();if(!i.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let a=typeof i.arguments?.pageId==`string`?i.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.execute`,{attributes:{"http.method":`POST`,"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":a}},async a=>{let s=t.getAll(e.C.Tool).find(e=>e.getDefinition().name===i.tool);if(process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`&&i.tool===`browser_launch`){let e=typeof i.arguments?.videoDir==`string`?i.arguments.videoDir:typeof i.arguments?.video_dir==`string`?i.arguments.video_dir:``;console.log(`[ExtensionRecordingDebug] http execute browser_launch mode=${String(i.arguments?.mode??``)} videoDir=${e}`)}if(!s)return a?.setStatus({code:p.SpanStatusCode.ERROR,message:`Tool "${i.tool}" not found`}),o.log(`warn`,`browse-tool HTTP execute rejected unknown tool`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool}}),n.json({success:!1,error:`Tool "${i.tool}" not found`},404);let c=await s.execute(i.arguments||{}),l=i.arguments||{},u=typeof l.pageId==`string`?l.pageId:void 0;if(u){let n=t.get(e.C.BrowserService),i=r.get(u);i&&n.recordBrowserActivity(i.browserId,u)}let d=Mt(c);return d?(a?.setStatus({code:p.SpanStatusCode.ERROR,message:d}),o.log(`error`,`browse-tool HTTP execute failed`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}})):o.log(`info`,`browse-tool HTTP execute succeeded`,{attributes:{"http.route":`/execute`,"browse_tool.tool.name":i.tool,"browse_tool.page.id":u}}),n.json({success:!c.isError,result:c,error:d})})}catch(e){return o.log(`error`,`browse-tool HTTP execute request crashed`,{attributes:{"http.route":`/execute`},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/browsers`,n=>{try{let r=t.get(e.C.BrowserService).listBrowsers();return n.json({browsers:r.map(e=>({id:e.id,profileName:e.profileName,pageIds:Array.from(e.pageIds),currentPageId:e.currentPageId,createdAt:e.createdAt.toISOString()}))})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/tools`,n=>{try{let r=t.getAll(e.C.Tool);return n.json({tools:r.map(e=>e.getDefinition())})}catch(e){return n.json({error:e instanceof Error?e.message:String(e)},500)}}),n.get(`/custom-tools`,async e=>{let t=e.req.query(`dir`);if(!t)return e.json({tools:[],error:`Missing "dir" query parameter`},400);try{let n=await s.listTools(t);return e.json({tools:n})}catch(t){return e.json({tools:[],error:t instanceof Error?t.message:String(t)},400)}}),n.post(`/custom-tools`,async n=>{let i=n.req.query(`dir`);if(!i)return n.json({success:!1,error:`Missing "dir" query parameter`},400);try{let a=await n.req.json();if(!a.tool)return n.json({success:!1,error:`Missing "tool" field in request body`},400);let c=typeof a.arguments?.pageId==`string`?a.arguments.pageId:void 0;return await o.runInSpan(`browse_tool.http.custom_tool.execute`,{attributes:{"http.method":`POST`,"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c,"browse_tool.custom_tools.directory":i}},async l=>{let u=await s.executeTool(i,a.tool,a.arguments||{});if(c){let n=t.get(e.C.BrowserService),i=r.get(c);i&&n.recordBrowserActivity(i.browserId,c)}let d=Mt(u);return d?(l?.setStatus({code:p.SpanStatusCode.ERROR,message:d}),o.log(`error`,`browse-tool custom tool execution failed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}})):o.log(`info`,`browse-tool custom tool execution succeeded`,{attributes:{"http.route":`/custom-tools`,"browse_tool.tool.name":a.tool,"browse_tool.page.id":c}}),n.json({success:!u.isError,result:u,error:d})})}catch(e){return o.log(`error`,`browse-tool custom tool request crashed`,{attributes:{"http.route":`/custom-tools`,"browse_tool.custom_tools.directory":i},exception:e}),n.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}});let c=w(t);n.route(`/extension`,c);let l=lt(t);n.route(`/api`,l);let f=jt(t);return n.route(`/`,f),n}function Ft(t,n,r){t.get(`/ws/extension/:browserId`,r(t=>{let r=t.req.param(`browserId`),i=null;return{onOpen(t,a){try{i=n.get(e.C.WebSocketHub).addConnection(a,r)}catch{a.close(1011,`Internal server error`)}},onMessage(t){if(i)try{let r=n.get(e.C.WebSocketHub),a=typeof t.data==`string`?t.data:t.data.toString();r.handleMessage(i,a)}catch{}},onClose(){if(i)try{n.get(e.C.WebSocketHub).removeConnection(i)}catch{}},onError(){if(i)try{n.get(e.C.WebSocketHub).removeConnection(i)}catch{}}}})),t.get(`/ws/stats`,t=>{try{let r=n.get(e.C.WebSocketHub).getStats();return t.json({totalConnections:r.totalConnections,browserCount:r.browserCount,connections:r.connections.map(e=>({id:e.id,browserId:e.browserId,connectedAt:e.connectedAt.toISOString()}))})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}})}const It=[`pnpm-workspace.yaml`,`nx.json`,`.git`];function Lt(e=process.cwd()){let t=l.default.resolve(e);for(;;){for(let e of It)if((0,c.existsSync)(l.default.join(t,e)))return t;let e=l.default.dirname(t);if(e===t)return process.cwd();t=e}}function Rt(e,t){let n=l.default.resolve(e),r=l.default.extname(n)===`.json`?l.default.dirname(n):n;return l.default.join(r,t)}async function zt(e,t){let n=await e.registerProcess({...t,force:!0});if(!n.success||!n.record)throw Error(n.error||`Failed to register process ${t.serviceName}`);let r=!1;return{release:async()=>{if(r)return;r=!0;let n=await e.releaseProcess({repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:t.serviceType,environment:t.environment,pid:t.pid,kill:!1,releasePort:!1});!n.success&&!n.error?.includes(`No matching process entry`)&&console.error(`Failed to release process registry entry for ${t.serviceName}: ${n.error}`)}}}const Bt=new t.Command(`http-serve`).description(`Start HTTP server for browser automation`).option(`-p, --port <port>`,`Port to listen on`,String(e.l)).option(`--host <host>`,`Host to bind`,e.d()).option(`--headless`,`Run browsers in headless mode by default`,!0).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before closing browsers`,`30`).option(`--registry-dir <path>`,`Custom registry path or directory for service discovery`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking (deprecated)`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).option(`--snippets-dir <path>`,`Directory used by browser_run_code to save and load reusable snippets`).action(async function(t){let r=M(`httpServe`),a={port:N(this,`port`,t.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),headless:N(this,`headless`,t.headless,r.headless),idleTimeout:N(this,`idleTimeout`,t.idleTimeout,r.idleTimeout===void 0?void 0:String(r.idleTimeout),process.env[e.c]),host:N(this,`host`,t.host,r.host,process.env.PLAYWRIGHT_HOST),registryDir:N(this,`registryDir`,t.registryDir,r.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),registryPath:N(this,`registryPath`,t.registryPath,r.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),pidsDir:N(this,`pidsDir`,t.pidsDir,r.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:N(this,`profilesDir`,t.profilesDir,r.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR),snippetsDir:N(this,`snippetsDir`,t.snippetsDir,r.snippetsDir,process.env.BROWSE_TOOL_SNIPPETS_DIR)},o;try{let t=Number.parseInt(a.port,10),r=`browse-tool-http`,s=process.env.NODE_ENV||`development`,c=Lt(process.cwd()),u=a.registryPath||a.registryDir||process.env.PLAYWRIGHT_REGISTRY_PATH;u&&(process.env.PLAYWRIGHT_REGISTRY_DIR=u,process.env.PORT_REGISTRY_PATH=u,process.env.PROCESS_REGISTRY_PATH=Rt(u,`processes.json`)),process.env.PLAYWRIGHT_HOST=a.host,process.env.PLAYWRIGHT_PORT=a.port,process.env[e.c]=a.idleTimeout,a.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=a.pidsDir),a.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=a.profilesDir),a.snippetsDir&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=l.default.resolve(a.snippetsDir)),console.log(`Starting HTTP server for browser automation...`),console.log(` Port: ${t}`),console.log(` Host: ${a.host}`),console.log(` Headless: ${a.headless}`),console.log(` Idle Timeout: ${a.idleTimeout} minutes`),process.env.BROWSE_TOOL_SNIPPETS_DIR&&console.log(` Snippets Dir: ${process.env.BROWSE_TOOL_SNIPPETS_DIR}`);let d=e.a(),f=d.get(e.C.ProcessRegistryService),p=Pt(d),{injectWebSocket:m,upgradeWebSocket:h}=(0,v.createNodeWebSocket)({app:p});Ft(p,d,h);let g=d.get(e.C.WebSocketHub),_=d.get(e.C.ExtensionTaskQueue),y=d.get(e.C.BrowserService),b=d.get(e.C.ExtensionSessionRegistry),x=d.get(e.C.PageRegistry),S=d.get(e.C.IdleCleanupService);S.start(),g.setEventHandlers({onSessionRegister:(e,t)=>{if(t.type===`session:register`){let n=t.payload.browserId||e.browserId,r=y.getBrowser(n),i,a;if(r&&(r.mode===`extension`||r.mode===`vm`)){i=r.id;let t=x.findByBrowser(i);a=r.currentPageId||t[0]?.id||``,g.reassignConnection(e.id,i),a||(a=x.registerExtensionPage(i),r.pageIds.add(a),r.currentPageId=a),console.log(`[WebSocket] Extension connected to existing browser: ${i}, pageId: ${a}`)}else{i=n,y.registerExtensionBrowserWithId(i),a=x.registerExtensionPage(i);let e=y.getBrowser(i);e&&(e.pageIds.add(a),e.currentPageId=a),console.log(`[WebSocket] Extension connected with new browser: ${i}, pageId: ${a}`)}let o=x.get(a);o&&(o.extensionTabId=t.payload.tabId,o.url=t.payload.url??o.url);let s=b.register({browserId:i,tabId:t.payload.tabId,url:t.payload.url,metadata:{transport:`websocket`}});g.sendSessionAck(e,t.id??``,s.id,s.controlMode),g.broadcastPageCreated(i,a,x.get(a)?.url)}},onTaskResult:(e,t)=>{if(t.type===`task:result`){let e=_.submitResult({taskId:t.payload.taskId,success:t.payload.success,result:t.payload.result,error:t.payload.error});e?.browserId&&y.recordBrowserActivity(e.browserId,e.pageId)}},onTabMapped:(e,t)=>{if(t.type===`tab:mapped`){let e=x.get(t.payload.pageId);if(!e)return;e.extensionTabId=t.payload.tabId,y.recordBrowserActivity(e.browserId,e.id)}},onDisconnect:e=>{e.sessionId&&b.removeSession(e.sessionId),console.log(`[WebSocket] Extension disconnected: ${e.browserId}`)}});let C=new i.PortRegistryService(process.env.PORT_REGISTRY_PATH);u?console.log(` Registry path: ${u}`):console.log(` Registry path: default (~/.port-registry/ports.json)`);let w=await C.reservePort({repositoryPath:c,serviceName:r,serviceType:`tool`,environment:s,preferredPort:t,pid:process.pid,host:a.host,force:!0,portRange:{min:t,max:t},metadata:{healthCheckUrl:`${e.u(a.host,t)}/health`,headless:a.headless,idleTimeout:a.idleTimeout}});if(!w.success||!w.record)throw Error(w.error||`Failed to reserve port ${t} in global registry`);let T=t,E;try{E=(0,n.serve)({fetch:p.fetch,port:T,hostname:a.host})}catch(e){throw await C.releasePort({repositoryPath:c,serviceName:r,serviceType:`tool`,environment:s,pid:process.pid}),e}m(E),o=await zt(f,{repositoryPath:c,serviceName:r,serviceType:`service`,environment:s,pid:process.pid,port:T,host:a.host,metadata:{healthCheckUrl:`${e.u(a.host,T)}/health`,headless:a.headless,idleTimeout:a.idleTimeout,transport:`http`}});let D=e.u(a.host,T);console.log(`HTTP server listening on ${D}`),console.log(` Health check: ${D}/health`),console.log(` Execute tool: POST ${D}/execute`),console.log(`
|
|
673
|
+
Press Ctrl+C to stop the server`);let O=async t=>{console.log(`\n\n${t} received. Shutting down gracefully...`);try{S.stop(),E.close(),console.log(`Server closed`);try{await d.get(e.C.BrowserService).closeAll(),console.log(`All browsers closed`)}catch{}try{o&&await o.release()}catch(e){console.error(`Process registry cleanup error:`,e)}try{await C.releasePort({repositoryPath:c,serviceName:r,serviceType:`tool`,environment:s,pid:process.pid}),console.log(`Service deregistered`)}catch{}console.log(`Goodbye!`),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>O(`SIGINT`)),process.on(`SIGTERM`,()=>O(`SIGTERM`))}catch(e){if(o)try{await o.release()}catch(e){console.error(`Process registry cleanup error:`,e)}console.error(`Error starting HTTP server:`,e),process.exit(1)}}),Vt={browser_click:[`input`],browser_fill:[`input`],browser_type:[`input`],browser_select:[`input`],browser_hover:[`input`],browser_drag:[`input`],browser_press_key:[`input`],browser_upload_file:[`input`],browser_navigate:[`navigation`],browser_go_back:[`navigation`],browser_go_forward:[`navigation`],browser_reload:[`navigation`],browser_wait_for:[`navigation`],browser_snapshot:[`snapshot`],browser_screenshot:[`snapshot`],browser_pdf:[`snapshot`],browser_list_pages:[`page`],browser_new_page:[`page`],browser_select_page:[`page`],browser_close_page:[`page`],browser_resize_page:[`page`,`emulation`],browser_handle_dialog:[`dialog`],browser_list_network_requests:[`network`],browser_get_network_request:[`network`],browser_list_console_messages:[`console`],browser_evaluate_script:[`script`],browser_emulate:[`emulation`],browser_expect:[`testing`],browser_start_trace:[`tracing`],browser_stop_trace:[`tracing`],browser_list_profiles:[`profile`],browser_delete_profile:[`profile`],run_spec:[`spec`,`testing`],discover_specs:[`spec`,`testing`],browser_launch:[`browser`],browser_close:[`browser`],browser_run_code:[`code`,`script`]};function Ht(e){let t=new Set;for(let[n,r]of Object.entries(Vt))r.some(t=>e.includes(t))&&t.add(n);return t}const Ut=3e3,Wt=4,Gt=150,Kt=[`browser_launch`],qt=[`browser_new_page`];function Jt(e){if(!e?.content?.[0]?.text)return null;try{let t=JSON.parse(e.content[0].text);return{browserId:t.browserId,pageId:t.pageId,mode:t.mode,url:t.url}}catch{return null}}async function Yt(e){await new Promise(t=>setTimeout(t,e))}async function Xt(e){let t=new AbortController,n=setTimeout(()=>t.abort(),Ut);try{let n=await fetch(`${e}/tools`,{signal:t.signal});if(!n.ok)throw Error(`HTTP error ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);if(!Array.isArray(r.tools))throw Error(`Invalid /tools response: missing tools array`);if(r.tools.length===0)throw Error(`HTTP server returned an empty tool list`);return r.tools}finally{clearTimeout(n)}}function Zt(e,t){let n=new URL(`/custom-tools`,e);return n.searchParams.set(`dir`,t),n.toString()}async function Qt(e,t){let n=new AbortController,r=setTimeout(()=>n.abort(),Ut);try{let r=await fetch(Zt(e,t),{signal:n.signal});if(!r.ok)throw Error(`HTTP error ${r.status}: ${r.statusText}`);let i=await r.json();if(i.error)throw Error(i.error);if(!Array.isArray(i.tools))throw Error(`Invalid /custom-tools response: missing tools array`);return i.tools}finally{clearTimeout(r)}}async function $t(e){let t;for(let n=1;n<=4;n+=1)try{return await Xt(e)}catch(e){t=e instanceof Error?e:Error(String(e)),n<4&&await Yt(150*n)}throw Error(`Failed to fetch tools after 4 attempts: ${t?.message??`Unknown error`}`,{cause:t})}async function en(e,t){let n;for(let r=1;r<=4;r+=1)try{return await Qt(e,t)}catch(e){n=e instanceof Error?e:Error(String(e)),r<4&&await Yt(150*r)}throw Error(`Failed to fetch custom tools after 4 attempts: ${n?.message??`Unknown error`}`,{cause:n})}function tn(e){if(!e?.tags?.length&&!e?.exclude?.length)return null;let t=e.tags?.length?Ht(e.tags):null,n=new Set(e.exclude??[]);if(!t)return n.size>0?n:null;for(let e of n)t.delete(e);return t}function nn(e){let{httpBaseUrl:t,sessionTracker:n,defaultMode:r,toolFilter:i,customToolsDir:o,enforcedProfileName:c}=e,l=i?.tags?.length?tn(i):null,u=new Set(i?.exclude??[]),d=l!==null||u.size>0,f=new a.Server({name:`browse-tool`,version:`0.1.0`},{capabilities:{tools:{}}}),p={name:`browser_list_session`,description:`List all browsers and pages created in this MCP session. Use this to see what resources you have available.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}},m=[],h=[],g=new Set;function _(e){return d?e.filter(e=>u.has(e.name)?!1:l===null?!0:l.has(e.name)):e}function v(e){return e.filter(e=>!u.has(e.name))}function y(e){return c?{...e,profileName:c}:e}function b(e){if(!c)return e;let t=e?.content?.[0]?.text;if(!t)return e;try{let n=JSON.parse(t),r=(Array.isArray(n.profiles)?n.profiles:[]).filter(e=>e?.name===c);return{...e,content:[{...e.content[0],text:JSON.stringify({...n,profileCount:r.length,profiles:r},null,2)},...e.content.slice(1)]}}catch{return e}}function x(e){return{name:e.name,description:e.description,inputSchema:e.inputSchema,annotations:e.capabilities}}function S(e){return u.has(e)?!1:l===null?!0:l.has(e)}return f.setRequestHandler(s.ListToolsRequestSchema,async()=>{try{let e=await $t(t);m=e,g=new Set(e.map(e=>e.name));let r=o?await en(t,o):[];h=r;let i=[..._(e),...v(r).map(e=>x(e))];return n&&i.push(p),{tools:i}}catch(e){if(m.length>0||h.length>0){console.error(`Failed to refresh tools from HTTP server. Returning cached tools:`,e instanceof Error?e.message:String(e));let t=[..._(m),...v(h).map(e=>x(e))];return n&&t.push(p),{tools:t}}let r=e instanceof Error?e.message:String(e);throw Error(`Failed to list tools from HTTP server at ${t}: ${r}`,{cause:e})}}),f.setRequestHandler(s.CallToolRequestSchema,async e=>{let{name:i,arguments:a}=e.params,s=new Set(h.map(e=>e.name));if(i!==`browser_list_session`&&!S(i))return{content:[{type:`text`,text:`Tool "${i}" is not available (filtered by --tags or --exclude)`}],isError:!0};if(i===`browser_list_session`&&n){let e=n.getSessionState();return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}let c=a||{};i===`browser_launch`&&r&&(c={...c,mode:r}),i===`browser_launch`&&(c=y(c));try{if(o&&!s.has(i)&&!g.has(i))try{h=await en(t,o),s=new Set(h.map(e=>e.name))}catch(e){console.error(`Failed to refresh custom tools, using cached list: ${e instanceof Error?e.message:String(e)}`)}let e=s.has(i),r=await fetch(e&&o?Zt(t,o):`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:i,arguments:c})});if(!r.ok){let e=await r.text();return{content:[{type:`text`,text:`HTTP error ${r.status}: ${e}`}],isError:!0}}let a=await r.json();if(!a.success)return{content:[{type:`text`,text:a.error||a.result?.content?.[0]?.text||`Unknown error from HTTP server`}],isError:!0};if(n&&a.result){if(Kt.includes(i)){let e=Jt(a.result);e?.browserId&&(n.trackBrowser(e.browserId,e.mode||`playwright`),e.pageId&&n.trackPage(e.pageId,e.browserId,e.url))}else if(qt.includes(i)){let e=Jt(a.result);e?.pageId&&e?.browserId&&n.trackPage(e.pageId,e.browserId,e.url)}}return i===`browser_list_profiles`&&a.result?b(a.result):a.result||{content:[{type:`text`,text:`No result returned`}]}}catch(e){return{content:[{type:`text`,text:`Failed to execute tool via HTTP server: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}),f}const J=`stdio`,rn=`http`,Y=`streamable-http`,an=new Set([J,rn,Y]),on=`chromium`,sn=new Set([on,`firefox`,`webkit`]),cn=new Set([`playwright`,`extension`,`vm`,`stealth`]),ln=`browser_close`,un=`,`,dn=`spawned`,fn=`reused`,X=`SIGINT`,Z=`SIGTERM`,pn=`PLAYWRIGHT_REGISTRY_DIR`,mn=`PORT_REGISTRY_PATH`,hn=`PLAYWRIGHT_PIDS_DIR`,gn=`PLAYWRIGHT_PROFILES_DIR`,_n=`PLAYWRIGHT_HOST`,vn=`PLAYWRIGHT_PORT`,yn=`PLAYWRIGHT_MCP_PORT`,bn=`x-profile`,xn=[`pnpm-workspace.yaml`,`nx.json`,`.git`],Q=`browse-tool-mcp-http`,Sn=`browse-tool-mcp`,Cn=`tool`,wn=`service`,Tn=0,En=1,Dn=3201;var On=class extends Error{code=`INVALID_TRANSPORT`;recovery=`Use --type ${[...an].join(`, `)}`;transportType;constructor(e,t){super(`Unknown transport type: ${e}`,t),this.name=`InvalidTransportError`,this.transportType=e}},kn=class extends Error{code=`INVALID_BROWSER_TYPE`;recovery=`Use --browser ${[...sn].join(`, `)}`;browserType;constructor(e,t){super(`Unknown browser type: ${e}`,t),this.name=`InvalidBrowserTypeError`,this.browserType=e}},An=class extends Error{code=`INVALID_LAUNCH_MODE`;recovery=`Use --mode ${[...cn].join(`, `)}`;launchMode;constructor(e,t){super(`Unknown launch mode: ${e}`,t),this.name=`InvalidLaunchModeError`,this.launchMode=e}};function jn(e){let t=e.toLowerCase();if(!sn.has(t))throw new kn(e);return t}function Mn(e){let t=e.toLowerCase();if(!cn.has(t))throw new An(e);return t}function Nn(e){let t=typeof e==`number`?e:Number.parseInt(e,10);if(!Number.isInteger(t)||t<=0||t>65535)throw Error(`Invalid port: ${e}`);return t}function Pn(e){let t=e.toLowerCase();return t===rn?Y:t}function Fn(e,t){let n=e[t];return(Array.isArray(n)?n[0]:n)?.trim()||void 0}function In(e=process.cwd()){let t=l.default.resolve(e);for(;;){for(let e of xn)if((0,c.existsSync)(l.default.join(t,e)))return t;let e=l.default.dirname(t);if(e===t)return process.cwd();t=e}}function Ln(e,t){let n=l.default.resolve(e),r=l.default.extname(n)===`.json`?l.default.dirname(n):n;return l.default.join(r,t)}async function Rn(e,t){let n=await e.registerProcess({...t,force:!0});if(!n.success||!n.record)throw Error(n.error||`Failed to register process ${t.serviceName}`);let r=!1;return{release:async()=>{if(r)return;r=!0;let n=await e.releaseProcess({repositoryPath:t.repositoryPath,serviceName:t.serviceName,serviceType:t.serviceType,environment:t.environment,pid:t.pid,kill:!1,releasePort:!1});!n.success&&!n.error?.includes(`No matching process entry`)&&console.error(`Failed to release process registry entry for ${t.serviceName}: ${n.error}`)}}}function zn(e){return e.split(`,`).map(e=>e.trim()).filter(e=>e.length>0)}function Bn(e){let t=e.tags?zn(e.tags):void 0,n=e.exclude?zn(e.exclude):void 0;if(!(!t?.length&&!n?.length))return{tags:t,exclude:n}}var Vn=class extends Error{code=`BROWSER_CLEANUP_FAILED`;recovery=`Browsers may still be running; check the HTTP server or kill processes manually`;failedBrowserIds;httpBaseUrl;constructor(e,t,n){super(`Failed to close ${e.length} browser(s): ${e.join(`, `)}`,n),this.name=`BrowserCleanupError`,this.failedBrowserIds=e,this.httpBaseUrl=t}},Hn=class extends Error{code=`SESSION_SHUTDOWN_FAILED`;recovery=`Check logs for cleanup and transport errors; processes may need manual cleanup`;signal;constructor(e,t){super(`Shutdown failed for signal ${e}`,t),this.name=`SessionShutdownError`,this.signal=e}},Un=class extends Error{code=`HTTP_SERVER_START_FAILED`;recovery=`Check if the HTTP server binary exists and the port is available`;constructor(e){super(`Failed to start HTTP server`,e),this.name=`HttpServerStartError`}},Wn=class extends Error{code=`MCP_SERVER_BOOTSTRAP_FAILED`;recovery=`Check if all dependencies are installed and try again`;constructor(e){super(`Failed to start MCP server`,e),this.name=`McpServerBootstrapError`}};async function Gn(e,t){let n=e.getBrowserIds(),r=[],i;for(let e of n)try{let n=await fetch(`${t}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tool:`browser_close`,arguments:{browserId:e}})});if(!n.ok){let e=await n.text().catch(()=>``);throw Error(`HTTP ${n.status} from ${t}/execute: ${e}`)}console.error(` Closed browser: ${e}`)}catch(t){let n=t instanceof Error?t:Error(String(t));console.error(` Failed to close browser: ${e}`,n),r.push(e),i??=n}if(r.length>0)throw new Vn(r,t,{cause:i});e.clear()}async function Kn(e,t,n,r){await e.start();let i=!1,a=async a=>{if(i)return;i=!0,console.error(`\nReceived ${a}, shutting down gracefully...`);let o;try{let e=t.getSessionState();e.totalBrowsers>0&&(console.error(` Cleaning up ${e.totalBrowsers} browser(s) from this session...`),await Gn(t,n))}catch(e){o=e instanceof Error?e:Error(String(e)),console.error(`Browser cleanup error:`,o)}finally{try{await e.stop()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Transport stop error:`,t),o??=t}try{await r?.()}catch(e){let t=e instanceof Error?e:Error(String(e));console.error(`Process registry cleanup error:`,t),o??=t}if(o){let e=new Hn(a,{cause:o});console.error(`Error during shutdown:`,e),process.exit(1)}process.exit(0)}};process.once(X,()=>a(X)),process.once(Z,()=>a(Z))}const qn=new t.Command(`mcp-serve`).description(`Start Playwright MCP server for browser automation`).option(`-t, --type <type>`,`Transport type: ${[J,Y].join(`, `)}`,J).option(`-b, --browser <browser>`,`Default browser type: ${[...sn].join(`, `)}`,on).option(`--headless`,`Run browsers in headless mode by default`,!1).option(`--no-headless`,`Run browsers in headed mode (visible window)`).option(`--host <host>`,`Default host for HTTP services`,e.d()).option(`--port <port>`,`Port for streamable HTTP transport`,`3201`).option(`-p, --profile <name>`,`Default profile name for browser sessions`).option(`-m, --mode <mode>`,`Default launch mode: ${[...cn].join(`, `)}`).option(`--tags <tags>`,`Comma-separated tags to filter tools (e.g. input,navigation,snapshot)`).option(`--exclude <tools>`,`Comma-separated tool names to exclude (applied after --tags)`).option(`--custom-tools <path>`,`Path to a folder containing tools.yaml and custom tool scripts`).option(`--snippets-dir <path>`,`Path to a folder where browser_run_code snippets are stored`).option(`--idle-timeout <minutes>`,`Idle timeout in minutes before auto-closing browsers`).option(`--registry-path <path>`,`Custom registry path or directory for service discovery`).option(`--registry-dir <path>`,`Custom registry directory for service discovery`).option(`--pids-dir <path>`,`Custom PIDs directory for process tracking`).option(`--profiles-dir <path>`,`Custom profiles directory for browser profiles`).action(async function(t){let n=M(`mcpServe`),r={type:N(this,`type`,t.type,n.type),browser:N(this,`browser`,t.browser,n.browser),headless:N(this,`headless`,t.headless,n.headless),profile:N(this,`profile`,t.profile,n.profile),mode:N(this,`mode`,t.mode,n.mode),host:N(this,`host`,t.host,n.host,process.env.PLAYWRIGHT_HOST),port:N(this,`port`,t.port,n.port,process.env.PLAYWRIGHT_MCP_PORT),tags:N(this,`tags`,t.tags,n.tags),exclude:N(this,`exclude`,t.exclude,n.exclude),customTools:N(this,`customTools`,t.customTools,n.customTools),snippetsDir:N(this,`snippetsDir`,t.snippetsDir,n.snippetsDir),idleTimeout:N(this,`idleTimeout`,t.idleTimeout,n.idleTimeout===void 0?void 0:String(n.idleTimeout),process.env[e.c]),registryPath:N(this,`registryPath`,t.registryPath,n.registryPath,process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH),registryDir:N(this,`registryDir`,t.registryDir,n.registryDir,process.env.PLAYWRIGHT_REGISTRY_DIR),pidsDir:N(this,`pidsDir`,t.pidsDir,n.pidsDir,process.env.PLAYWRIGHT_PIDS_DIR),profilesDir:N(this,`profilesDir`,t.profilesDir,n.profilesDir,process.env.PLAYWRIGHT_PROFILES_DIR)},a=Pn(r.type),o=In(process.cwd()),s=process.env.NODE_ENV||`development`,c;try{if(!an.has(a))throw new On(a);let t=jn(r.browser),n=r.mode?Mn(r.mode):void 0,u=Nn(r.port??3201),d=Bn(r),f=r.customTools?l.default.resolve(r.customTools):void 0,p=r.snippetsDir?l.default.resolve(r.snippetsDir):void 0;console.error(`Playwright MCP Server starting...`),console.error(` Transport: ${a}`),a===Y&&console.error(` MCP endpoint: http://${r.host??e.d()}:${u}/mcp`),console.error(` Default browser: ${t}`),console.error(` Headless: ${r.headless}`),n&&console.error(` Default mode: ${n}`),r.profile&&console.error(` Profile: ${r.profile}`),d?.tags?.length&&console.error(` Tags: ${d.tags.join(`, `)}`),d?.exclude?.length&&console.error(` Exclude: ${d.exclude.join(`, `)}`),f&&console.error(` Custom tools: ${f}`),p&&console.error(` Snippets dir: ${p}`),r.idleTimeout&&console.error(` Idle timeout: ${r.idleTimeout} minutes`);let m=r.registryPath||r.registryDir;m&&(process.env.PLAYWRIGHT_REGISTRY_DIR=m,process.env.PORT_REGISTRY_PATH=m,process.env.PROCESS_REGISTRY_PATH=Ln(m,`processes.json`),console.error(` Registry path: ${m}`)),r.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=r.pidsDir,console.error(` PIDs dir: ${r.pidsDir}`)),r.profilesDir&&(process.env.PLAYWRIGHT_PROFILES_DIR=r.profilesDir,console.error(` Profiles dir: ${r.profilesDir}`)),p&&(process.env.BROWSE_TOOL_SNIPPETS_DIR=p),r.idleTimeout&&(process.env[e.c]=r.idleTimeout),r.host&&(process.env.PLAYWRIGHT_HOST=r.host),process.env.PLAYWRIGHT_PORT=e.f().toString();let h=e.o(),g=h.get(e.C.ProcessRegistryService),_=await h.get(e.C.HttpServerManager).ensureRunning();if(!_.running)throw new Un;let v=e.u(e.d(),_.port),y=_.spawned?`spawned`:`reused`;if(console.error(` HTTP server: ${y} on port ${_.port}`),a===J){let i=new e.s,a=new e.n(nn({httpBaseUrl:v,sessionTracker:i,defaultMode:n,toolFilter:d,customToolsDir:f,enforcedProfileName:r.profile}));c=await Rn(g,{repositoryPath:o,serviceName:`browse-tool-mcp`,serviceType:wn,environment:s,pid:process.pid,metadata:{transport:J,browser:t,mode:n}}),await Kn(a,i,v,async()=>{await c?.release()});return}let b=new i.PortRegistryService(process.env.PORT_REGISTRY_PATH),x=await b.reservePort({repositoryPath:o,serviceName:Q,serviceType:Cn,environment:s,preferredPort:u,pid:process.pid,host:r.host??e.d(),force:!0,portRange:{min:u,max:u},metadata:{healthCheckUrl:`${e.u(r.host??e.d(),u)}/health`,transport:Y}});if(!x.success||!x.record)throw Error(x.error||`Failed to reserve ${Y} port ${u}`);let S=x.record.port;c=await Rn(g,{repositoryPath:o,serviceName:Q,serviceType:wn,environment:s,pid:process.pid,port:S,host:r.host??e.d(),metadata:{healthCheckUrl:`${e.u(r.host??e.d(),S)}/health`,transport:Y}});let C=new e.t(({headers:t})=>{let i=Fn(t,`x-profile`)??r.profile,a=new e.s;return{server:nn({httpBaseUrl:v,sessionTracker:a,defaultMode:n,toolFilter:d,customToolsDir:f,enforcedProfileName:i}),onClose:async()=>{a.getSessionState().totalBrowsers>0&&await Gn(a,v)}}},{host:r.host??e.d(),port:S});try{await C.start()}catch(e){try{await c.release()}catch(e){console.error(`Process registry cleanup error:`,e)}throw await b.releasePort({repositoryPath:o,serviceName:Q,serviceType:Cn,environment:s,pid:process.pid}),e}let w=!1,T=async e=>{if(!w){w=!0,console.error(`\nReceived ${e}, shutting down gracefully...`);try{await C.stop();try{await c?.release()}catch(e){console.error(`Process registry cleanup error:`,e)}await b.releasePort({repositoryPath:o,serviceName:Q,serviceType:Cn,environment:s,pid:process.pid}),process.exit(0)}catch(e){console.error(`Transport stop error:`,e),process.exit(1)}}};process.once(X,()=>T(X)),process.once(Z,()=>T(Z))}catch(e){if(c)try{await c.release()}catch(e){console.error(`Process registry cleanup error:`,e)}(e instanceof On||e instanceof kn||e instanceof An||e instanceof Un)&&(console.error(`Error [${e.code}]: ${e.message}`),console.error(`Recovery: ${e.recovery}`),process.exit(1));let t=new Wn({cause:e instanceof Error?e:Error(String(e))});console.error(`Error [${t.code}]: ${t.message}`,t.cause),console.error(`Recovery: ${t.recovery}`),process.exit(1)}}),Jn=new t.Command(`status`).description(`Show status of HTTP server and diagnostics`).action(async()=>{try{let t=M(`httpServe`),n=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??t.registryPath??t.registryDir;n&&(process.env.PLAYWRIGHT_REGISTRY_DIR=n,process.env.PLAYWRIGHT_REGISTRY_PATH=n,process.env.PORT_REGISTRY_PATH=n),!process.env.PLAYWRIGHT_PIDS_DIR&&t.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=t.pidsDir),console.log(`browse-tool Status
|
|
674
|
+
`),console.log(`-`.repeat(50));let r=await e.i().get(e.C.HttpServerManager).getStatus();if(r.running){let n=process.env.PLAYWRIGHT_HOST??t.host??e.d();console.log(`HTTP Server: Running`),console.log(` PID: ${r.pid}`),console.log(` Port: ${r.port}`),console.log(` Health: ${e.u(n,r.port)}/health`),r.browserCount!==void 0&&console.log(` Browsers: ${r.browserCount}`)}else console.log(`HTTP Server: Not Running`),r.error&&console.log(` Error: ${r.error}`);console.log(``),console.log(`-`.repeat(50)),process.exit(0)}catch(e){console.error(`Error getting status:`,e),process.exit(1)}}),Yn=new t.Command(`stop`).description(`Stop HTTP server and clean up registry/PID files`).action(async()=>{try{let t=M(`httpServe`),n=process.env.PLAYWRIGHT_REGISTRY_PATH??process.env.PORT_REGISTRY_PATH??t.registryPath??t.registryDir;n&&(process.env.PLAYWRIGHT_REGISTRY_DIR=n,process.env.PLAYWRIGHT_REGISTRY_PATH=n,process.env.PORT_REGISTRY_PATH=n),!process.env.PLAYWRIGHT_PIDS_DIR&&t.pidsDir&&(process.env.PLAYWRIGHT_PIDS_DIR=t.pidsDir),console.log(`Stopping browse-tool services...`),await e.i().get(e.C.HttpServerManager).stop()?(console.log(`HTTP server stopped`),console.log(`Registry and PID files cleaned up`)):console.log(`No HTTP server was running`),console.log(`Done!`),process.exit(0)}catch(e){console.error(`Error stopping services:`,e),process.exit(1)}}),Xn=new Set([`--config`]);function Zn(e){for(let t=0;t<e.length;t+=1){let n=e[t];if(Xn.has(n)){t+=1;continue}if(![...Xn].some(e=>n.startsWith(`${e}=`))&&!n.startsWith(`-`))return n}}function Qn(e,t=!1){if(t)return!1;let n=Zn(e);return n?n===`tools`?!0:n===`help`&&e.includes(`tools`):!1}function $n(e){return e.replace(/_/g,`-`)}function $(e){return e.replace(/([a-z])([A-Z])/g,`$1-$2`).toLowerCase()}function er(e){let t=new Set;e.type&&t.add(e.type);for(let n of e.oneOf??[])n.type&&t.add(n.type);return t}function tr(e,t){let n=er(t);if(t.type===`number`||t.type===`integer`){let t=Number(e);if(Number.isNaN(t))throw Error(`Invalid number: ${e}`);return t}if(t.type===`boolean`){if(e===`true`||e===`1`)return!0;if(e===`false`||e===`0`)return!1;throw Error(`Invalid boolean: ${e}`)}if(n.has(`array`)||n.has(`object`))try{return JSON.parse(e)}catch{if(t.type===`array`||t.type===`object`)throw Error(`Invalid JSON: ${e}`)}if(n.has(`number`)||n.has(`integer`)){let t=Number(e);if(!Number.isNaN(t)&&e.trim()!==``)return t}return e}function nr(e,t){let n=$(e);return t.type===`boolean`?`--${n}`:`--${n} <value>`}function rr(n){let{definition:r,execute:i}=n,a=new t.Command($n(r.name));a.description(r.description);let o=r.inputSchema,s=o.properties||{},c=new Set(o.required||[]);for(let[e,n]of Object.entries(s)){let r=n,i=nr(e,r),o=r.description||``;r.enum&&r.enum.length>0&&(o+=` (choices: ${r.enum.join(`, `)})`),r.default!==void 0&&(o+=` (default: ${JSON.stringify(r.default)})`),c.has(e)&&(o+=` [required]`),r.type===`boolean`?r.default===!0?a.option(`--no-${$(e)}`,`Disable ${o}`):a.option(i,o,r.default):r.enum&&r.enum.length>0?a.addOption(new t.Option(i,o).choices(r.enum)):a.option(i,o)}return a.action(async function(){let t=this.optsWithGlobals(),n=M(`tools`),a=N(this,`format`,t.format??`json`,n.format),o=N(this,`color`,t.color??!0,n.color),l=N(this,`port`,String(t.port??e.l),n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),u={format:a,color:o},d=Ce(r.name);try{let e={};for(let[n,r]of Object.entries(s)){let i=r,a=$(n).replace(/-([a-z])/g,(e,t)=>t.toUpperCase()),o=this.getOptionValueSourceWithGlobals(a),s=t[a];i.type===`boolean`&&i.default===!0&&(s=t[a]),(o===void 0||o===`default`||o===`implied`)&&d[n]!==void 0&&(s=d[n]),s!==void 0&&(typeof s==`string`&&i.type!==`string`?e[n]=tr(s,i):e[n]=s)}for(let t of c)if(e[t]===void 0){let e=$(t);console.error(L(`Missing required option: --${e}`,u.color)),process.exit(1)}let n=Number.parseInt(l,10),a=P({port:n}),o=i?await i(e,{command:this,formatterOptions:u,port:n}):await a.execute(r.name,e),f=I(o,u);f&&console.log(f),o.isError&&process.exit(1)}catch(e){console.error(L(e instanceof Error?e:String(e),u.color)),process.exit(1)}}),a}function ir(e){return e.map(rr)}const ar=`browser_list_session`,or=`List browsers and pages currently tracked by the browse-tool HTTP server.`,sr={name:`browser_list_session`,description:`List browsers and pages currently tracked by the browse-tool HTTP server.`,inputSchema:{type:`object`,properties:{},required:[],additionalProperties:!1}};function cr(){return new t.Command(`tools`).description(`Execute browser automation tools`).option(`-f, --format <format>`,`Output format: json, text, quiet`,`json`).option(`--no-color`,`Disable colored output`).option(`-p, --port <port>`,`HTTP server port`,String(e.l))}function lr(e){return{browserCount:e.length,browsers:e.map(e=>({browserId:e.id,profileName:e.profileName,currentPageId:e.currentPageId,pageIds:e.pageIds,createdAt:e.createdAt}))}}function ur(){return{definition:sr,execute:async(e,t)=>{let n=await P({port:t.port}).listBrowsers();return{content:[{type:`text`,text:JSON.stringify(lr(n),null,2)}]}}}}function dr(e){return[...e.map(e=>({definition:e})),ur()]}async function fr(t,n){let r=M(`tools`),i=Number(n?.port??process.env.PLAYWRIGHT_PORT??r.port??e.l);try{let e=ir(dr(await P({port:i}).listTools()));for(let n of e)t.addCommand(n)}catch(e){let t=e instanceof Error?e.message:String(e);process.stderr.write(`${L(`Failed to load tool commands: ${t}`,!0)}\n`),process.stderr.write(`Make sure the HTTP server is running: browse-tool http-serve
|
|
675
|
+
`)}}const pr=`PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS`;function mr(e,t){let n=[...e];for(let e=0;e<n.length;e+=1){let t=n[e];if(t===`--port`||t===`-p`){let t=n[e+1],r=Number(t);if(!Number.isNaN(r)&&t)return r;continue}if(t.startsWith(`--port=`)){let e=Number(t.slice(7));if(!Number.isNaN(e))return e;continue}if(t.startsWith(`-p=`)){let e=Number(t.slice(3));if(!Number.isNaN(e))return e}}return Number(t)}async function hr(){let n=Se(process.argv.slice(2)),r=process.env.PLAYWRIGHT_PORT??n.config.commands.tools?.port??n.config.commands.exec?.port??e.l,i=new t.Command;i.name(`browse-tool`).description(`MCP server for browser automation using Playwright with profile management, page registry, and multi-browser support`).option(`--config <path>`,`Path to browse-tool config file or config directory`).version(x),i.addCommand(qn),i.addCommand(Bt),i.addCommand(le),i.addCommand(Re),i.addCommand(Yn),i.addCommand(Jn),i.addCommand(ze),i.addCommand(Fe),i.addCommand(Ie);let a=cr();Qn(process.argv.slice(2),process.env.PLAYWRIGHT_SKIP_DYNAMIC_TOOL_COMMANDS===`1`)&&await fr(a,{port:mr(process.argv.slice(2),r)}),i.addCommand(a),await i.parseAsync(process.argv)}hr().catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exit(1)});
|
|
676
676
|
//# sourceMappingURL=cli.cjs.map
|