@agimon-ai/browse-tool 0.15.1 → 0.15.2

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const e=require(`./streamable-http-UNuTiSXW.cjs`);require(`reflect-metadata/lite`);let t=require(`@agimon-ai/foundation-port-registry`),n=require(`@agimon-ai/foundation-process-registry`),r=require(`inversify`),i=require(`node:path`);i=e.k(i,1);let a=require(`node:fs`),o=require(`node:fs/promises`),s=require(`node:os`),c=require(`node:module`),l=require(`zod`),u=require(`@opentelemetry/api`),d=require(`node:crypto`),f=require(`@modelcontextprotocol/sdk/server/index.js`),p=require(`@modelcontextprotocol/sdk/types.js`),m=require(`@modelcontextprotocol/sdk/server/stdio.js`),h=require(`commander`),g=require(`@hono/node-server`),_=require(`hono`),v=require(`hono/cors`),y=require(`@hono/node-ws`),b=require(`hono/html`),x=require(`hono/jsx`),S=require(`hono/jsx/jsx-runtime`);var C=`0.15.0`;const w=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function T(e,t){if(!w)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function E(t){let n=new _.Hono,r;try{r=t.get(e.O.TelemetryService)}catch{r=new e.g}return n.get(`/telemetry-config`,t=>{try{let n=new URL(t.req.url).origin;return t.json(e._(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.O.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.O.ExtensionTaskQueue),a=t.get(e.O.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.O.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.O.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.O.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.O.PageRegistry),a=t.get(e.O.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.O.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return T(`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.O.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return T(`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:{}}}),T(`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.O.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.O.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.O.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 D(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 O=[`pnpm-workspace.yaml`,`nx.json`,`.git`],k=`browse-tool-chrome`,A=`tool`,j=process.env.NODE_ENV||`development`,ee=`127.0.0.1`;function te(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of O)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}function ne(){return new t.PortRegistryService(process.env.PORT_REGISTRY_PATH)}async function re(e,t,n){let r=ne(),i=await r.reservePort({repositoryPath:e,serviceName:k,serviceType:A,environment:j,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:k,serviceType:A,environment:j,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function ie(){return new r.ContainerModule(t=>{t.bind(e.O.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.O.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function ae(e){let t=new f.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(p.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(p.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const oe=new h.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 i=>{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 a,o;try{let s=D(i.port),c=te(process.cwd()),l=process.env.PORT_REGISTRY_PATH;l&&(process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(l,`processes.json`)),a=await re(c,s??t.DEFAULT_PORT_RANGE.min,s?{min:s,max:s}:t.DEFAULT_PORT_RANGE);let u=a.port;o=await(0,n.createProcessLease)({repositoryPath:c,serviceName:k,serviceType:A,environment:j,pid:process.pid,port:u,host:ee,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:i.waitForExtension}}),i.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${u}`),console.error(` Wait for extension: ${i.waitForExtension}`));let d=new r.Container({defaultScope:`Singleton`});d.load(ie());let f=d.get(e.O.ExtensionTaskQueue),p=d.get(e.O.ExtensionToolDelegator),h=new _.Hono;h.use(`*`,(0,v.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let y=E(d);h.route(`/extension`,y),h.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,g.serve)({fetch:h.fetch,port:u});b.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${u} 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 ${u}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${u}/extension/tasks`),i.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{f.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let x=ae(p),S=new m.StdioServerTransport;await x.connect(S),console.error(`Chrome extension MCP server started on stdio`);let C=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{f.clearAllTasks(`Server shutting down`),await S.close(),b.close(),await o.release({kill:!1}),await a.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>C(`SIGINT`)),process.on(`SIGTERM`,()=>C(`SIGTERM`))}catch(e){if(o||a)try{await o?.release(),await a?.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)}}),se=`BROWSE_TOOL_CONFIG`,ce=`.browse-tool`,le=`config.json`,ue=[`json`,`text`,`quiet`],de={commands:{},tools:{}},fe=l.z.record(l.z.string(),l.z.unknown()),pe=l.z.object({mcpServe:l.z.object({type:l.z.string().optional(),browser:l.z.string().optional(),headless:l.z.boolean().optional(),profile:l.z.string().optional(),mode:l.z.string().optional(),host:l.z.string().optional(),port:l.z.coerce.number().int().positive().optional(),httpPort:l.z.coerce.number().int().positive().optional(),tags:l.z.string().optional(),exclude:l.z.string().optional(),customTools:l.z.string().optional(),chromeForTestingPath:l.z.string().optional(),snippetsDir:l.z.string().optional(),registryPath:l.z.string().optional(),registryDir:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),httpServe:l.z.object({port:l.z.coerce.number().int().positive().optional(),headless:l.z.boolean().optional(),idleTimeout:l.z.coerce.number().positive().optional(),host:l.z.string().optional(),registryDir:l.z.string().optional(),registryPath:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),snippetsDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),exec:l.z.object({format:l.z.enum(ue).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),tools:l.z.object({format:l.z.enum(ue).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),status:l.z.record(l.z.string(),l.z.unknown()).optional(),stop:l.z.record(l.z.string(),l.z.unknown()).optional()}),me=l.z.object({commands:pe.default({}),tools:l.z.record(l.z.string(),fe).default({})});let M={config:de};function he(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 ge(e){let t=i.default.resolve(e);if(!(0,a.existsSync)(t))throw Error(`Config path not found: ${t}`);return(0,a.statSync)(t).isDirectory()?i.default.join(t,le):t}function _e(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),o=t.homeDir??(0,s.homedir)(),c=he(e);if(c)return ge(c);if(n[se])return ge(n[se]);let l=i.default.join(r,ce,le);if((0,a.existsSync)(l))return l;let u=i.default.join(o,ce,le);if((0,a.existsSync)(u))return u}function ve(e){let t=(0,a.readFileSync)(e,`utf8`),n=JSON.parse(t);return me.parse(n)}function ye(e,t={}){let n=_e(e,t);return n?{configPath:n,config:ve(n)}:{config:de}}function be(e,t={}){return M=ye(e,t),M}function N(e){return(M.config.commands??{})[e]??{}}function xe(e){return M.config.tools?.[e]??{}}function P(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}var Se=class{port;exactPort;timeout;serverPort=null;constructor(t={}){this.port=t.port??e.d,this.exactPort=t.exactPort??!1,this.timeout=t.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await e.o().get(e.O.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 F(e){return new Se(e)}const Ce={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 I(e,t,n){return n?`${Ce[t]}${e}${Ce.reset}`:e}function we(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Te(e):Ee(e,r)}function Te(e){return JSON.stringify(e,null,2)}function Ee(e,t){let n=[];e.isError&&n.push(I(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(De(r,t)):r.type===`image`?n.push(Ae(r,t)):n.push(I(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
2
+ const e=require(`./streamable-http-UNuTiSXW.cjs`);require(`reflect-metadata/lite`);let t=require(`@agimon-ai/foundation-port-registry`),n=require(`@agimon-ai/foundation-process-registry`),r=require(`inversify`),i=require(`node:path`);i=e.k(i,1);let a=require(`node:fs`),o=require(`node:fs/promises`),s=require(`node:os`),c=require(`node:module`),l=require(`zod`),u=require(`@opentelemetry/api`),d=require(`node:crypto`),f=require(`@modelcontextprotocol/sdk/server/index.js`),p=require(`@modelcontextprotocol/sdk/types.js`),m=require(`@modelcontextprotocol/sdk/server/stdio.js`),h=require(`commander`),g=require(`@hono/node-server`),_=require(`hono`),v=require(`hono/cors`),y=require(`@hono/node-ws`),b=require(`hono/html`),x=require(`hono/jsx`),S=require(`hono/jsx/jsx-runtime`);var C=`0.15.1`;const w=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function T(e,t){if(!w)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function E(t){let n=new _.Hono,r;try{r=t.get(e.O.TelemetryService)}catch{r=new e.g}return n.get(`/telemetry-config`,t=>{try{let n=new URL(t.req.url).origin;return t.json(e._(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.O.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.O.ExtensionTaskQueue),a=t.get(e.O.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.O.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.O.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.O.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.O.PageRegistry),a=t.get(e.O.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.O.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return T(`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.O.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return T(`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:{}}}),T(`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.O.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.O.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.O.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 D(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 O=[`pnpm-workspace.yaml`,`nx.json`,`.git`],k=`browse-tool-chrome`,A=`tool`,j=process.env.NODE_ENV||`development`,ee=`127.0.0.1`;function te(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of O)if((0,a.existsSync)(i.default.join(t,e)))return t;let e=i.default.dirname(t);if(e===t)return process.cwd();t=e}}function ne(){return new t.PortRegistryService(process.env.PORT_REGISTRY_PATH)}async function re(e,t,n){let r=ne(),i=await r.reservePort({repositoryPath:e,serviceName:k,serviceType:A,environment:j,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:k,serviceType:A,environment:j,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function ie(){return new r.ContainerModule(t=>{t.bind(e.O.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.O.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function ae(e){let t=new f.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(p.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(p.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const oe=new h.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 i=>{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 a,o;try{let s=D(i.port),c=te(process.cwd()),l=process.env.PORT_REGISTRY_PATH;l&&(process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(l,`processes.json`)),a=await re(c,s??t.DEFAULT_PORT_RANGE.min,s?{min:s,max:s}:t.DEFAULT_PORT_RANGE);let u=a.port;o=await(0,n.createProcessLease)({repositoryPath:c,serviceName:k,serviceType:A,environment:j,pid:process.pid,port:u,host:ee,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:i.waitForExtension}}),i.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${u}`),console.error(` Wait for extension: ${i.waitForExtension}`));let d=new r.Container({defaultScope:`Singleton`});d.load(ie());let f=d.get(e.O.ExtensionTaskQueue),p=d.get(e.O.ExtensionToolDelegator),h=new _.Hono;h.use(`*`,(0,v.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let y=E(d);h.route(`/extension`,y),h.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,g.serve)({fetch:h.fetch,port:u});b.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${u} 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 ${u}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${u}/extension/tasks`),i.waitForExtension&&(console.error(`Waiting for extension connection before starting MCP...`),await new Promise(e=>{let t=setInterval(()=>{f.getConnectionStatus().connected&&(clearInterval(t),console.error(`Extension connected!`),e())},500)}));let x=ae(p),S=new m.StdioServerTransport;await x.connect(S),console.error(`Chrome extension MCP server started on stdio`);let C=async e=>{console.error(`\nReceived ${e}, shutting down gracefully...`);try{f.clearAllTasks(`Server shutting down`),await S.close(),b.close(),await o.release({kill:!1}),await a.release(),process.exit(0)}catch(e){console.error(`Error during shutdown:`,e),process.exit(1)}};process.on(`SIGINT`,()=>C(`SIGINT`)),process.on(`SIGTERM`,()=>C(`SIGTERM`))}catch(e){if(o||a)try{await o?.release(),await a?.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)}}),se=`BROWSE_TOOL_CONFIG`,ce=`.browse-tool`,le=`config.json`,ue=[`json`,`text`,`quiet`],de={commands:{},tools:{}},fe=l.z.record(l.z.string(),l.z.unknown()),pe=l.z.object({mcpServe:l.z.object({type:l.z.string().optional(),browser:l.z.string().optional(),headless:l.z.boolean().optional(),profile:l.z.string().optional(),mode:l.z.string().optional(),host:l.z.string().optional(),port:l.z.coerce.number().int().positive().optional(),httpPort:l.z.coerce.number().int().positive().optional(),tags:l.z.string().optional(),exclude:l.z.string().optional(),customTools:l.z.string().optional(),chromeForTestingPath:l.z.string().optional(),snippetsDir:l.z.string().optional(),registryPath:l.z.string().optional(),registryDir:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),httpServe:l.z.object({port:l.z.coerce.number().int().positive().optional(),headless:l.z.boolean().optional(),idleTimeout:l.z.coerce.number().positive().optional(),host:l.z.string().optional(),registryDir:l.z.string().optional(),registryPath:l.z.string().optional(),pidsDir:l.z.string().optional(),profilesDir:l.z.string().optional(),snippetsDir:l.z.string().optional(),proxyConfigDir:l.z.string().optional()}).partial().optional(),exec:l.z.object({format:l.z.enum(ue).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),tools:l.z.object({format:l.z.enum(ue).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),status:l.z.record(l.z.string(),l.z.unknown()).optional(),stop:l.z.record(l.z.string(),l.z.unknown()).optional()}),me=l.z.object({commands:pe.default({}),tools:l.z.record(l.z.string(),fe).default({})});let M={config:de};function he(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 ge(e){let t=i.default.resolve(e);if(!(0,a.existsSync)(t))throw Error(`Config path not found: ${t}`);return(0,a.statSync)(t).isDirectory()?i.default.join(t,le):t}function _e(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),o=t.homeDir??(0,s.homedir)(),c=he(e);if(c)return ge(c);if(n[se])return ge(n[se]);let l=i.default.join(r,ce,le);if((0,a.existsSync)(l))return l;let u=i.default.join(o,ce,le);if((0,a.existsSync)(u))return u}function ve(e){let t=(0,a.readFileSync)(e,`utf8`),n=JSON.parse(t);return me.parse(n)}function ye(e,t={}){let n=_e(e,t);return n?{configPath:n,config:ve(n)}:{config:de}}function be(e,t={}){return M=ye(e,t),M}function N(e){return(M.config.commands??{})[e]??{}}function xe(e){return M.config.tools?.[e]??{}}function P(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}var Se=class{port;exactPort;timeout;serverPort=null;constructor(t={}){this.port=t.port??e.d,this.exactPort=t.exactPort??!1,this.timeout=t.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let t=await e.o().get(e.O.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 F(e){return new Se(e)}const Ce={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 I(e,t,n){return n?`${Ce[t]}${e}${Ce.reset}`:e}function we(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Te(e):Ee(e,r)}function Te(e){return JSON.stringify(e,null,2)}function Ee(e,t){let n=[];e.isError&&n.push(I(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(De(r,t)):r.type===`image`?n.push(Ae(r,t)):n.push(I(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
3
3
  `)}function De(e,t){let n=e.text;try{return Oe(JSON.parse(n),t)}catch{return n}}function Oe(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=I(r,`cyan`,t),a=ke(i,t);n.push(`${e}: ${a}`)}return n.join(`
4
4
  `)}function ke(e,t){return e===null?I(`null`,`gray`,t):e===void 0?I(`undefined`,`gray`,t):typeof e==`boolean`?I(String(e),e?`green`:`red`,t):typeof e==`number`?I(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?I(e,`blue`,t):e:Array.isArray(e)?e.length===0?I(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function Ae(e,t){let{mimeType:n,data:r}=e;return I(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function L(e,t){return I(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function je(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=I(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
5
5
  `)}function Me(e,t){let n=N(`tools`),r=P(e,`format`,t.format,n.format),i=P(e,`color`,t.color,n.color);return{port:P(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Ne=new h.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.d)).action(async function(e,t){let{port:n,formatterOptions:r}=Me(this,t);try{let t=await F({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?je(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)}}),Pe=new h.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.d)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=Me(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 F({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=we(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)}});new h.Command(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Ne).addCommand(Pe);const Fe=new h.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.D).option(`--image <image>`,`Docker image tag to produce`,e.T).option(`--platform <platform>`,`Docker target platform`,e.E).action(async t=>{try{let n=await e.S({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.C(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Ie=new h.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.d)).action(async function(e,t,n){let r=N(`exec`),i=P(this,`format`,n.format,r.format),a=P(this,`color`,n.color,r.color),o=P(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 F({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=we(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)}}),R=`pageId`,z=`browserId`,Le=l.z.record(l.z.string(),l.z.unknown()),Re=l.z.object({type:l.z.literal(`object`),properties:l.z.record(l.z.string(),l.z.unknown()).optional(),required:l.z.array(l.z.string()).optional(),additionalProperties:l.z.boolean().optional()}).passthrough(),ze=l.z.object({name:l.z.string().min(1),description:l.z.string().min(1),script:l.z.string().min(1),suggestionActions:l.z.string().min(1).optional(),capabilities:Le,inputSchema:Re}),Be=l.z.object({tools:l.z.array(ze)}),Ve=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function He(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function B(e,t){if(Ve){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function V(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ue(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 We(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 Ge(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(`-`)?Ke(e,t,n):qe(e,t,n)}function Ke(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]=Je(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]=Je(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 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,o]=Je(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 Je(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 Ye(e){let t=Ge(e);if(t.length===0)return{};let[n]=U(t,0,t[0].indent);return n}function Xe(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!V(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[R],r=t[z];if(n!==void 0&&!(V(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(V(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[R]={type:`string`,description:`Browse-tool page ID to run this custom tool against. Either pageId or browserId is required at call time.`}),r===void 0&&(t[z]={type:`string`,description:`Browse-tool browser ID to run this custom tool against when no pageId is supplied. The tool will use the browser's current page or open a new one.`})}async function Ze(e){let t=(0,c.stripTypeScriptTypes)(await(0,o.readFile)(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function Qe(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 $e(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function et(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=$e(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 tt(e,t,n){if(Ue(t))return n?et(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?$e(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var nt=class{constructor(t,n,r=new e.g,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.v(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(t,n){let r=typeof n[R]==`string`&&n[R].length>0?n[R]:void 0,i=typeof n[z]==`string`&&n[z].length>0?n[z]:void 0;if(!r&&!i)throw Error(`Custom tool "${t}" requires a string pageId or browserId`);if(r){let n=this.pageRegistry.get(r);if(!n)throw Error(`Page "${r}" not found`);let a=e.b();if(a&&n.ownerId&&n.ownerId!==a)throw Error(`Custom tool "${t}" cannot use page "${r}" owned by another session`);if(i&&n.browserId!==i)throw Error(`Custom tool "${t}" received pageId "${r}" for browser "${n.browserId}", not "${i}"`);let o=this.createBrowserHelper(t,n.browserId);return{pageId:r,pageEntry:n,page:this.resolveToolPage(t,r,n),browser:o}}let a=this.createBrowserHelper(t,i);try{let e=await a.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:a}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await a.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:a}}}createBrowserHelper(t,n){return{browserId:n,mode:this.browserService?.getBrowser(n)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(n);if(!e)throw Error(`Browser "${n}" not found`);return this.pageRegistry.findByBrowser(n).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async e=>{let r=this.pageRegistry.get(e);if(!r||r.browserId!==n)throw Error(`Page "${e}" not found in browser "${n}"`);let i=this.browserService?.getBrowser(n);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(t,e,r)}},getCurrentPage:async()=>{let r=this.browserService?.getBrowser(n);if(!r)throw Error(`Browser "${n}" not found`);let i=e.b(),a;if(i){let e=this.pageRegistry.findByOwner(i).filter(e=>e.browserId===n);if(a=e[e.length-1]?.id,!a)throw Error(`Browser "${n}" has no pages owned by this session`)}else a=r.currentPageId??this.pageRegistry.findByBrowser(n)[0]?.id??void 0;if(!a)throw Error(`Browser "${n}" has no pages`);let o=this.pageRegistry.get(a);if(!o)throw Error(`Page "${a}" not found`);return{...this.toPageSummary(r.currentPageId,o),page:this.resolveToolPage(t,a,o)}},newPage:async e=>this.createPageForBrowser(t,n,e)}}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:We({...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,a=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":i.default.resolve(e),"browse_tool.page.id":r,"browse_tool.browser.id":a}},async r=>{let a=(await this.loadTools(e)).find(e=>e.name===t);if(!a)throw r?.setStatus({code:u.SpanStatusCode.ERROR,message:`Custom tool "${t}" not found`}),Error(`Custom tool "${t}" not found`);let{pageId:o,pageEntry:s,page:c,browser:l}=await this.resolveExecutionContext(t,n),d=this.createToolLogger(t,o,s);r?.setAttributes({"browse_tool.browser.id":s.browserId,"browse_tool.execution.mode":s.mode,"browse_tool.page.id":o}),B(`Executing custom tool`,{toolName:t,directory:i.default.resolve(e),pageId:o,browserId:s.browserId,mode:s.mode,input:He(n),scriptPath:a.scriptPath});let f;try{f=await a.execute?.({page:c,browser:l,input:n,logger:d})}catch(e){let n=e instanceof Error?e.message:String(e);throw B(`Custom tool execution failed`,{toolName:t,pageId:o,browserId:s.browserId,mode:s.mode,error:n,stack:e instanceof Error?e.stack:void 0}),Error(`Custom tool "${t}" failed on page "${o}": ${n}`,{cause:e})}B(`Custom tool execution completed`,{toolName:t,pageId:o,browserId:s.browserId,mode:s.mode,result:He(f)});let p=tt(t,f,a.suggestionActions),m=p.isError?p.content[0]?.text:void 0;return m&&r?.setStatus({code:u.SpanStatusCode.ERROR,message:m}),p})}async loadTools(e){let t=i.default.resolve(e),n=i.default.join(t,`tools.yaml`),r=Ye(await(0,o.readFile)(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),a=Be.parse(r);return Promise.all(a.tools.map(async e=>{Xe(e);let n=i.default.resolve(t,e.script);await(0,o.stat)(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=Qe(n,await Ze(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function rt(t){let n=new _.Hono;return n.get(`/browsers`,async n=>{try{let r=t.get(e.O.BrowserService),i=t.get(e.O.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.O.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.O.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.O.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 it(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 at(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 ot(e){return at(new Date().getTime()-e.getTime())}const st=`table-container`,ct=`stat-card`,lt=`browser-row`,ut=`page-row`,W=`status-active`,dt=`url-cell`,G=`timestamp-cell`,K=`actions-cell`,q=`kill-btn`,ft=`empty-state`;function pt({browsers:e}){return e.length===0?(0,S.jsx)(`div`,{class:st,children:(0,S.jsxs)(`div`,{class:ft,children:[(0,S.jsx)(`h3`,{children:`No Active Browsers`}),(0,S.jsx)(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):(0,S.jsx)(`div`,{class:st,children:(0,S.jsxs)(`table`,{class:`browser-table`,children:[(0,S.jsx)(`thead`,{children:(0,S.jsxs)(`tr`,{children:[(0,S.jsx)(`th`,{children:`ID`}),(0,S.jsx)(`th`,{children:`Status`}),(0,S.jsx)(`th`,{children:`URL / Title`}),(0,S.jsx)(`th`,{children:`Created`}),(0,S.jsx)(`th`,{children:`Age`}),(0,S.jsx)(`th`,{children:`Actions`})]})}),(0,S.jsx)(`tbody`,{id:`browser-table-body`,children:e.map(e=>(0,S.jsxs)(x.Fragment,{children:[(0,S.jsxs)(`tr`,{class:lt,children:[(0,S.jsx)(`td`,{children:e.id}),(0,S.jsx)(`td`,{children:(0,S.jsxs)(`span`,{class:W,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),(0,S.jsx)(`td`,{children:e.profileName||`Default Profile`}),(0,S.jsx)(`td`,{class:G,children:it(e.createdAt)}),(0,S.jsx)(`td`,{class:G,children:ot(e.createdAt)}),(0,S.jsx)(`td`,{class:K,children:(0,S.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>(0,S.jsxs)(`tr`,{class:ut,children:[(0,S.jsxs)(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),(0,S.jsx)(`td`,{children:(0,S.jsx)(`span`,{class:W,children:`Open`})}),(0,S.jsx)(`td`,{class:dt,title:t.url,children:t.title||t.url||`about:blank`}),(0,S.jsx)(`td`,{class:G,children:it(t.createdAt)}),(0,S.jsx)(`td`,{class:G,children:ot(t.createdAt)}),(0,S.jsx)(`td`,{class:K,children:(0,S.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function mt({stats:e}){return(0,S.jsxs)(`div`,{class:`stats-container`,children:[(0,S.jsxs)(`div`,{class:ct,children:[(0,S.jsx)(`h3`,{children:`Active Browsers`}),(0,S.jsx)(`div`,{class:`value`,children:e.totalBrowsers})]}),(0,S.jsxs)(`div`,{class:ct,children:[(0,S.jsx)(`h3`,{children:`Active Pages`}),(0,S.jsx)(`div`,{class:`value`,children:e.totalPages})]})]})}function ht({browsers:e,stats:t}){return(0,S.jsxs)(`div`,{class:`dashboard-container`,children:[(0,S.jsxs)(`div`,{class:`dashboard-header`,children:[(0,S.jsxs)(`div`,{children:[(0,S.jsx)(`h1`,{children:`Playwright MCP Dashboard`}),(0,S.jsx)(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),(0,S.jsxs)(`div`,{children:[(0,S.jsx)(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),(0,S.jsx)(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),(0,S.jsx)(mt,{stats:t}),(0,S.jsx)(pt,{browsers:e}),(0,S.jsx)(`script`,{children:(0,b.raw)(`
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{C as e,D as t,E as n,O as r,S as i,T as a,_ as o,a as s,b as c,c as l,d as u,f as d,g as f,h as p,i as m,l as h,m as g,n as _,o as v,p as y,s as b,t as ee,u as x,v as S,w as C,x as w,y as T}from"./streamable-http-CZoE_y6A.mjs";import{stripTypeScriptTypes as E}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as D,PortRegistryService as O}from"@agimon-ai/foundation-port-registry";import{createProcessLease as k,resolveSiblingRegistryPath as te}from"@agimon-ai/foundation-process-registry";import{Container as A,ContainerModule as ne}from"inversify";import j from"node:path";import{existsSync as M,readFileSync as re,statSync as ie}from"node:fs";import{readFile as N,stat as ae}from"node:fs/promises";import{homedir as oe}from"node:os";import{z as P}from"zod";import{SpanStatusCode as F}from"@opentelemetry/api";import{randomUUID as se}from"node:crypto";import{Server as ce}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as le,ListToolsRequestSchema as ue}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as de}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as I,Option as fe}from"commander";import{serve as pe}from"@hono/node-server";import{Hono as L}from"hono";import{cors as me}from"hono/cors";import{createNodeWebSocket as he}from"@hono/node-ws";import{raw as ge}from"hono/html";import{Fragment as _e}from"hono/jsx";import{jsx as R,jsxs as z}from"hono/jsx/jsx-runtime";var ve=`0.15.0`;const ye=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function be(e,t){if(!ye)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function xe(e){let t=new L,n;try{n=e.get(r.TelemetryService)}catch{n=new f}return t.get(`/telemetry-config`,e=>{try{let t=new URL(e.req.url).origin;return e.json(o(process.env,t))}catch(t){return e.json({enabled:!1,error:t instanceof Error?t.message:String(t)},500)}}),t.get(`/tasks`,t=>{try{let n=e.get(r.ExtensionTaskQueue).getNextTask();return n?t.json({task:{id:n.id,tool:n.tool,arguments:n.arguments,telemetry:n.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let n=await t.req.json();if(!n.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(r.ExtensionTaskQueue),a=e.get(r.BrowserService),o={taskId:n.taskId,success:n.success,result:n.result,error:n.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),t.json({success:!0})):t.json({success:!1,error:`Task ${n.taskId} not found or already completed`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/status`,t=>{try{let n=e.get(r.ExtensionTaskQueue),i=n.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:n.queueSize};return t.json(a)}catch(e){return t.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/register`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).register(n);return t.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/heartbeat`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).heartbeat(n);return i?t.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/tab-mapped`,async t=>{try{let n=await t.req.json();if(!n.pageId||typeof n.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(r.PageRegistry),a=e.get(r.BrowserService),o=i.get(n.pageId);return o?(o.extensionTabId=n.tabId,a.recordBrowserActivity(o.browserId,n.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${n.pageId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.BrowserService),a=await i.persistExtensionRecordingArtifact(n.browserId,n.videoBase64);return be(`artifact received`,{browserId:n.browserId,videoBase64Size:n.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(n.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${n.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording/chunk`,async t=>{try{let i=await t.req.json();if(!i.browserId||!i.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(r.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return be(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(n.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),t.json({success:!0})):t.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.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):(n.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),be(`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)}}),t.post(`/handoff`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).requestHandoff(n);return i?t.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/handoff/acknowledge`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).acknowledgeHandoff(n.sessionId);return i?t.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found or no handoff pending`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/sessions`,t=>{try{let n=e.get(r.ExtensionSessionRegistry).listSessions();return t.json({sessions:n.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 t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function Se(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 Ce=[`pnpm-workspace.yaml`,`nx.json`,`.git`],we=`browse-tool-chrome`,Te=`tool`,Ee=process.env.NODE_ENV||`development`,De=`127.0.0.1`;function Oe(e=process.cwd()){let t=j.resolve(e);for(;;){for(let e of Ce)if(M(j.join(t,e)))return t;let e=j.dirname(t);if(e===t)return process.cwd();t=e}}function ke(){return new O(process.env.PORT_REGISTRY_PATH)}async function Ae(e,t,n){let r=ke(),i=await r.reservePort({repositoryPath:e,serviceName:we,serviceType:Te,environment:Ee,preferredPort:t,portRange:n,pid:process.pid,host:De,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:we,serviceType:Te,environment:Ee,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function je(){return new ne(e=>{e.bind(r.ExtensionTaskQueue).to(p).inSingletonScope(),e.bind(r.ExtensionToolDelegator).to(g).inSingletonScope()})}function Me(e){let t=new ce({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(ue,async()=>({tools:n})),t.setRequestHandler(le,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ne=new I(`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 e=>{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 t,n;try{let i=Se(e.port),a=Oe(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=te(o,`processes.json`)),t=await Ae(a,i??D.min,i?{min:i,max:i}:D);let s=t.port;n=await k({repositoryPath:a,serviceName:we,serviceType:Te,environment:Ee,pid:process.pid,port:s,host:De,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:e.waitForExtension}}),e.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${s}`),console.error(` Wait for extension: ${e.waitForExtension}`));let c=new A({defaultScope:`Singleton`});c.load(je());let l=c.get(r.ExtensionTaskQueue),u=c.get(r.ExtensionToolDelegator),d=new L;d.use(`*`,me({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=xe(c);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=pe({fetch:d.fetch,port:s});p.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${s} 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 ${s}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${s}/extension/tasks`),e.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=Me(u),h=new de;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(),await n.release({kill:!1}),await t.release(),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){if(n||t)try{await n?.release(),await t?.release()}catch{}let r=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${r}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),Pe=`BROWSE_TOOL_CONFIG`,Fe=`.browse-tool`,Ie=`config.json`,Le=[`json`,`text`,`quiet`],Re={commands:{},tools:{}},ze=P.record(P.string(),P.unknown()),Be=P.object({mcpServe:P.object({type:P.string().optional(),browser:P.string().optional(),headless:P.boolean().optional(),profile:P.string().optional(),mode:P.string().optional(),host:P.string().optional(),port:P.coerce.number().int().positive().optional(),httpPort:P.coerce.number().int().positive().optional(),tags:P.string().optional(),exclude:P.string().optional(),customTools:P.string().optional(),chromeForTestingPath:P.string().optional(),snippetsDir:P.string().optional(),registryPath:P.string().optional(),registryDir:P.string().optional(),pidsDir:P.string().optional(),profilesDir:P.string().optional(),proxyConfigDir:P.string().optional()}).partial().optional(),httpServe:P.object({port:P.coerce.number().int().positive().optional(),headless:P.boolean().optional(),idleTimeout:P.coerce.number().positive().optional(),host:P.string().optional(),registryDir:P.string().optional(),registryPath:P.string().optional(),pidsDir:P.string().optional(),profilesDir:P.string().optional(),snippetsDir:P.string().optional(),proxyConfigDir:P.string().optional()}).partial().optional(),exec:P.object({format:P.enum(Le).optional(),color:P.boolean().optional(),port:P.coerce.number().int().positive().optional()}).partial().optional(),tools:P.object({format:P.enum(Le).optional(),color:P.boolean().optional(),port:P.coerce.number().int().positive().optional()}).partial().optional(),status:P.record(P.string(),P.unknown()).optional(),stop:P.record(P.string(),P.unknown()).optional()}),Ve=P.object({commands:Be.default({}),tools:P.record(P.string(),ze).default({})});let B={config:Re};function He(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 Ue(e){let t=j.resolve(e);if(!M(t))throw Error(`Config path not found: ${t}`);return ie(t).isDirectory()?j.join(t,Ie):t}function We(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??oe(),a=He(e);if(a)return Ue(a);if(n[Pe])return Ue(n[Pe]);let o=j.join(r,Fe,Ie);if(M(o))return o;let s=j.join(i,Fe,Ie);if(M(s))return s}function Ge(e){let t=re(e,`utf8`),n=JSON.parse(t);return Ve.parse(n)}function Ke(e,t={}){let n=We(e,t);return n?{configPath:n,config:Ge(n)}:{config:Re}}function qe(e,t={}){return B=Ke(e,t),B}function V(e){return(B.config.commands??{})[e]??{}}function Je(e){return B.config.tools?.[e]??{}}function H(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}var Ye=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??u,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await v().get(r.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!e.running||!e.port)throw Error(e.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=e.port,e.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 U(e){return new Ye(e)}const Xe={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 W(e,t,n){return n?`${Xe[t]}${e}${Xe.reset}`:e}function Ze(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Qe(e):$e(e,r)}function Qe(e){return JSON.stringify(e,null,2)}function $e(e,t){let n=[];e.isError&&n.push(W(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(et(r,t)):r.type===`image`?n.push(rt(r,t)):n.push(W(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
2
+ import{C as e,D as t,E as n,O as r,S as i,T as a,_ as o,a as s,b as c,c as l,d as u,f as d,g as f,h as p,i as m,l as h,m as g,n as _,o as v,p as y,s as b,t as ee,u as x,v as S,w as C,x as w,y as T}from"./streamable-http-CZoE_y6A.mjs";import{stripTypeScriptTypes as E}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as D,PortRegistryService as O}from"@agimon-ai/foundation-port-registry";import{createProcessLease as k,resolveSiblingRegistryPath as te}from"@agimon-ai/foundation-process-registry";import{Container as A,ContainerModule as ne}from"inversify";import j from"node:path";import{existsSync as M,readFileSync as re,statSync as ie}from"node:fs";import{readFile as N,stat as ae}from"node:fs/promises";import{homedir as oe}from"node:os";import{z as P}from"zod";import{SpanStatusCode as F}from"@opentelemetry/api";import{randomUUID as se}from"node:crypto";import{Server as ce}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as le,ListToolsRequestSchema as ue}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as de}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as I,Option as fe}from"commander";import{serve as pe}from"@hono/node-server";import{Hono as L}from"hono";import{cors as me}from"hono/cors";import{createNodeWebSocket as he}from"@hono/node-ws";import{raw as ge}from"hono/html";import{Fragment as _e}from"hono/jsx";import{jsx as R,jsxs as z}from"hono/jsx/jsx-runtime";var ve=`0.15.1`;const ye=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function be(e,t){if(!ye)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function xe(e){let t=new L,n;try{n=e.get(r.TelemetryService)}catch{n=new f}return t.get(`/telemetry-config`,e=>{try{let t=new URL(e.req.url).origin;return e.json(o(process.env,t))}catch(t){return e.json({enabled:!1,error:t instanceof Error?t.message:String(t)},500)}}),t.get(`/tasks`,t=>{try{let n=e.get(r.ExtensionTaskQueue).getNextTask();return n?t.json({task:{id:n.id,tool:n.tool,arguments:n.arguments,telemetry:n.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let n=await t.req.json();if(!n.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(r.ExtensionTaskQueue),a=e.get(r.BrowserService),o={taskId:n.taskId,success:n.success,result:n.result,error:n.error},s=i.submitResult(o);return s?(s.browserId&&a.recordBrowserActivity(s.browserId,s.pageId),t.json({success:!0})):t.json({success:!1,error:`Task ${n.taskId} not found or already completed`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/status`,t=>{try{let n=e.get(r.ExtensionTaskQueue),i=n.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:n.queueSize};return t.json(a)}catch(e){return t.json({connected:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/register`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).register(n);return t.json({success:!0,session:{id:i.id,browserId:i.browserId,controlMode:i.controlMode,createdAt:i.createdAt.toISOString()}})}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/heartbeat`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).heartbeat(n);return i?t.json({success:!0,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested,lastHeartbeatAt:i.lastHeartbeatAt.toISOString()}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/tab-mapped`,async t=>{try{let n=await t.req.json();if(!n.pageId||typeof n.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(r.PageRegistry),a=e.get(r.BrowserService),o=i.get(n.pageId);return o?(o.extensionTabId=n.tabId,a.recordBrowserActivity(o.browserId,n.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${n.pageId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording`,async t=>{try{let n=await t.req.json();if(!n.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(r.BrowserService),a=await i.persistExtensionRecordingArtifact(n.browserId,n.videoBase64);return be(`artifact received`,{browserId:n.browserId,videoBase64Size:n.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(n.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${n.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/recording/chunk`,async t=>{try{let i=await t.req.json();if(!i.browserId||!i.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(r.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return be(`chunk received`,{browserId:i.browserId,chunkIndex:i.chunkIndex,mimeType:i.mimeType,persisted:!!o,chunkBase64Size:i.chunkBase64.length}),o?(n.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),t.json({success:!0})):t.json({success:!1,error:`Browser "${i.browserId}" has no active recording target`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.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):(n.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),be(`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)}}),t.post(`/handoff`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).requestHandoff(n);return i?t.json({success:!0,message:`Handoff requested. AI will take control when ready.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/handoff/acknowledge`,async t=>{try{let n=await t.req.json();if(!n.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(r.ExtensionSessionRegistry).acknowledgeHandoff(n.sessionId);return i?t.json({success:!0,message:`Handoff acknowledged. AI now has control.`,session:{id:i.id,controlMode:i.controlMode,handoffRequested:i.handoffRequested}}):t.json({success:!1,error:`Session ${n.sessionId} not found or no handoff pending`},404)}catch(e){return t.json({success:!1,error:e instanceof Error?e.message:String(e)},500)}}),t.get(`/sessions`,t=>{try{let n=e.get(r.ExtensionSessionRegistry).listSessions();return t.json({sessions:n.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 t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function Se(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 Ce=[`pnpm-workspace.yaml`,`nx.json`,`.git`],we=`browse-tool-chrome`,Te=`tool`,Ee=process.env.NODE_ENV||`development`,De=`127.0.0.1`;function Oe(e=process.cwd()){let t=j.resolve(e);for(;;){for(let e of Ce)if(M(j.join(t,e)))return t;let e=j.dirname(t);if(e===t)return process.cwd();t=e}}function ke(){return new O(process.env.PORT_REGISTRY_PATH)}async function Ae(e,t,n){let r=ke(),i=await r.reservePort({repositoryPath:e,serviceName:we,serviceType:Te,environment:Ee,preferredPort:t,portRange:n,pid:process.pid,host:De,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:we,serviceType:Te,environment:Ee,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function je(){return new ne(e=>{e.bind(r.ExtensionTaskQueue).to(p).inSingletonScope(),e.bind(r.ExtensionToolDelegator).to(g).inSingletonScope()})}function Me(e){let t=new ce({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(ue,async()=>({tools:n})),t.setRequestHandler(le,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ne=new I(`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 e=>{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 t,n;try{let i=Se(e.port),a=Oe(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=te(o,`processes.json`)),t=await Ae(a,i??D.min,i?{min:i,max:i}:D);let s=t.port;n=await k({repositoryPath:a,serviceName:we,serviceType:Te,environment:Ee,pid:process.pid,port:s,host:De,command:process.argv[1],args:process.argv.slice(2),metadata:{transport:`stdio`,command:`chrome-serve`,waitForExtension:e.waitForExtension}}),e.verbose&&(console.error(`Chrome Extension MCP Server starting...`),console.error(` HTTP Port: ${s}`),console.error(` Wait for extension: ${e.waitForExtension}`));let c=new A({defaultScope:`Singleton`});c.load(je());let l=c.get(r.ExtensionTaskQueue),u=c.get(r.ExtensionToolDelegator),d=new L;d.use(`*`,me({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=xe(c);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=pe({fetch:d.fetch,port:s});p.on(`error`,e=>{e.code===`EADDRINUSE`?(console.error(`Error [PORT_IN_USE]: Port ${s} 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 ${s}`),console.error(`Waiting for Chrome extension to connect...`),console.error(`Extension should poll: http://localhost:${s}/extension/tasks`),e.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=Me(u),h=new de;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(),await n.release({kill:!1}),await t.release(),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){if(n||t)try{await n?.release(),await t?.release()}catch{}let r=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${r}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),Pe=`BROWSE_TOOL_CONFIG`,Fe=`.browse-tool`,Ie=`config.json`,Le=[`json`,`text`,`quiet`],Re={commands:{},tools:{}},ze=P.record(P.string(),P.unknown()),Be=P.object({mcpServe:P.object({type:P.string().optional(),browser:P.string().optional(),headless:P.boolean().optional(),profile:P.string().optional(),mode:P.string().optional(),host:P.string().optional(),port:P.coerce.number().int().positive().optional(),httpPort:P.coerce.number().int().positive().optional(),tags:P.string().optional(),exclude:P.string().optional(),customTools:P.string().optional(),chromeForTestingPath:P.string().optional(),snippetsDir:P.string().optional(),registryPath:P.string().optional(),registryDir:P.string().optional(),pidsDir:P.string().optional(),profilesDir:P.string().optional(),proxyConfigDir:P.string().optional()}).partial().optional(),httpServe:P.object({port:P.coerce.number().int().positive().optional(),headless:P.boolean().optional(),idleTimeout:P.coerce.number().positive().optional(),host:P.string().optional(),registryDir:P.string().optional(),registryPath:P.string().optional(),pidsDir:P.string().optional(),profilesDir:P.string().optional(),snippetsDir:P.string().optional(),proxyConfigDir:P.string().optional()}).partial().optional(),exec:P.object({format:P.enum(Le).optional(),color:P.boolean().optional(),port:P.coerce.number().int().positive().optional()}).partial().optional(),tools:P.object({format:P.enum(Le).optional(),color:P.boolean().optional(),port:P.coerce.number().int().positive().optional()}).partial().optional(),status:P.record(P.string(),P.unknown()).optional(),stop:P.record(P.string(),P.unknown()).optional()}),Ve=P.object({commands:Be.default({}),tools:P.record(P.string(),ze).default({})});let B={config:Re};function He(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 Ue(e){let t=j.resolve(e);if(!M(t))throw Error(`Config path not found: ${t}`);return ie(t).isDirectory()?j.join(t,Ie):t}function We(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??oe(),a=He(e);if(a)return Ue(a);if(n[Pe])return Ue(n[Pe]);let o=j.join(r,Fe,Ie);if(M(o))return o;let s=j.join(i,Fe,Ie);if(M(s))return s}function Ge(e){let t=re(e,`utf8`),n=JSON.parse(t);return Ve.parse(n)}function Ke(e,t={}){let n=We(e,t);return n?{configPath:n,config:Ge(n)}:{config:Re}}function qe(e,t={}){return B=Ke(e,t),B}function V(e){return(B.config.commands??{})[e]??{}}function Je(e){return B.config.tools?.[e]??{}}function H(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}var Ye=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??u,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await v().get(r.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!e.running||!e.port)throw Error(e.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=e.port,e.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 U(e){return new Ye(e)}const Xe={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 W(e,t,n){return n?`${Xe[t]}${e}${Xe.reset}`:e}function Ze(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Qe(e):$e(e,r)}function Qe(e){return JSON.stringify(e,null,2)}function $e(e,t){let n=[];e.isError&&n.push(W(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(et(r,t)):r.type===`image`?n.push(rt(r,t)):n.push(W(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
3
3
  `)}function et(e,t){let n=e.text;try{return tt(JSON.parse(n),t)}catch{return n}}function tt(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=W(r,`cyan`,t),a=nt(i,t);n.push(`${e}: ${a}`)}return n.join(`
4
4
  `)}function nt(e,t){return e===null?W(`null`,`gray`,t):e===void 0?W(`undefined`,`gray`,t):typeof e==`boolean`?W(String(e),e?`green`:`red`,t):typeof e==`number`?W(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?W(e,`blue`,t):e:Array.isArray(e)?e.length===0?W(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function rt(e,t){let{mimeType:n,data:r}=e;return W(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function G(e,t){return W(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function it(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=W(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
5
5
  `)}function at(e,t){let n=V(`tools`),r=H(e,`format`,t.format,n.format),i=H(e,`color`,t.color,n.color);return{port:H(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const ot=new I(`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(u)).action(async function(e,t){let{port:n,formatterOptions:r}=at(this,t);try{let t=await U({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?it(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(G(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),st=new I(`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(u)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=at(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(G(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await U({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=Ze(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(G(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new I(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(ot).addCommand(st);const ct=new I(`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`,t).option(`--image <image>`,`Docker image tag to produce`,a).option(`--platform <platform>`,`Docker target platform`,n).action(async t=>{try{let n=await i({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(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),lt=new I(`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(u)).action(async function(e,t,n){let r=V(`exec`),i=H(this,`format`,n.format,r.format),a=H(this,`color`,n.color,r.color),o=H(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(G(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await U({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=Ze(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(G(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),K=`pageId`,q=`browserId`,ut=P.record(P.string(),P.unknown()),dt=P.object({type:P.literal(`object`),properties:P.record(P.string(),P.unknown()).optional(),required:P.array(P.string()).optional(),additionalProperties:P.boolean().optional()}).passthrough(),ft=P.object({name:P.string().min(1),description:P.string().min(1),script:P.string().min(1),suggestionActions:P.string().min(1).optional(),capabilities:ut,inputSchema:dt}),pt=P.object({tools:P.array(ft)}),mt=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function ht(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function gt(e,t){if(mt){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function J(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function _t(e){return J(e)&&Array.isArray(e.content)}function Y(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 vt(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 yt(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 X(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(`-`)?bt(e,t,n):xt(e,t,n)}function bt(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]=X(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=St(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]=X(e,i+1,r.indent);s[t]=a,i=o}else s[t]=Y(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]=X(e,i,t.indent);if(!J(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=St(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]=X(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=Y(a),i+=1}r.push(s);continue}r.push(Y(a)),i+=1}return[r,i]}function xt(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]=St(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]=X(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=Y(o),i+=1}return[r,i]}function St(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 Ct(e){let t=yt(e);if(t.length===0)return{};let[n]=X(t,0,t[0].indent);return n}function wt(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!J(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[K],r=t[q];if(n!==void 0&&!(J(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(J(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[K]={type:`string`,description:`Browse-tool page ID to run this custom tool against. Either pageId or browserId is required at call time.`}),r===void 0&&(t[q]={type:`string`,description:`Browse-tool browser ID to run this custom tool against when no pageId is supplied. The tool will use the browser's current page or open a new one.`})}async function Tt(e){let t=E(await N(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function Et(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 Dt(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(J(r)){let i=Dt(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 kt(e,t,n){if(_t(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=J(t)&&n?Dt(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var At=class{constructor(e,t,n=new f,r){this.pageRegistry=e,this.extensionTaskQueue=t,this.telemetry=n,this.browserService=r}resolveToolPage(e,t,n){if(n.page)return n.page;if(n.mode===`extension`&&this.extensionTaskQueue){let e=new S(this.extensionTaskQueue);return e.setTarget(t,n.browserId),e}throw Error(`Custom tool "${e}" 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[K]==`string`&&t[K].length>0?t[K]:void 0,r=typeof t[q]==`string`&&t[q].length>0?t[q]: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`);let i=c();if(i&&t.ownerId&&t.ownerId!==i)throw Error(`Custom tool "${e}" cannot use page "${n}" owned by another session`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let a=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:a}}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=c(),i;if(r){let e=this.pageRegistry.findByOwner(r).filter(e=>e.browserId===t);if(i=e[e.length-1]?.id,!i)throw Error(`Browser "${t}" has no pages owned by this session`)}else i=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!i)throw Error(`Browser "${t}" has no pages`);let a=this.pageRegistry.get(i);if(!a)throw Error(`Page "${i}" not found`);return{...this.toPageSummary(n.currentPageId,a),page:this.resolveToolPage(e,i,a)}},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:vt({...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[K]==`string`?n[K]:void 0,i=typeof n[q]==`string`?n[q]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":j.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:F.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}),gt(`Executing custom tool`,{toolName:t,directory:j.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:ht(n),scriptPath:i.scriptPath});let u;try{u=await i.execute?.({page:s,browser:c,input:n,logger:l})}catch(e){let n=e instanceof Error?e.message:String(e);throw gt(`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})}gt(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:ht(u)});let d=kt(t,u,i.suggestionActions),f=d.isError?d.content[0]?.text:void 0;return f&&r?.setStatus({code:F.ERROR,message:f}),d})}async loadTools(e){let t=j.resolve(e),n=j.join(t,`tools.yaml`),r=Ct(await N(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=pt.parse(r);return Promise.all(i.tools.map(async e=>{wt(e);let n=j.resolve(t,e.script);await ae(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=Et(n,await Tt(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function jt(e){let t=new L;return t.get(`/browsers`,async t=>{try{let n=e.get(r.BrowserService),i=e.get(r.PageRegistry),a=n.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 t.json({browsers:s,stats:c})}catch(e){return console.error(`Failed to list browsers:`,e),t.json({error:`Failed to list browsers`,message:e instanceof Error?e.message:String(e)},500)}}),t.delete(`/browsers`,async t=>{try{return await e.get(r.BrowserService).closeAll(),t.json({success:!0,message:`All browsers closed`})}catch(e){return console.error(`Failed to close all browsers:`,e),t.json({error:`Failed to close all browsers`,message:e instanceof Error?e.message:String(e)},500)}}),t.delete(`/browsers/:id`,async t=>{try{let n=t.req.param(`id`),i=e.get(r.BrowserService);return i.getBrowser(n)?(await i.closeBrowser(n),t.json({success:!0,message:`Browser "${n}" closed`})):t.json({error:`Browser "${n}" not found`},404)}catch(e){return console.error(`Failed to close browser:`,e),t.json({error:`Failed to close browser`,message:e instanceof Error?e.message:String(e)},500)}}),t.delete(`/pages/:id`,async t=>{try{let n=t.req.param(`id`),i=e.get(r.PageRegistry).get(n);return i?i.page?(await i.page.close(),t.json({success:!0,message:`Page "${n}" closed`})):t.json({error:`Page "${n}" is in extension mode and cannot be closed via API`},400):t.json({error:`Page "${n}" not found`},404)}catch(e){return console.error(`Failed to close page:`,e),t.json({error:`Failed to close page`,message:e instanceof Error?e.message:String(e)},500)}}),t}function Mt(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 Nt(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 Pt(e){return Nt(new Date().getTime()-e.getTime())}const Ft=`table-container`,It=`stat-card`,Lt=`browser-row`,Rt=`page-row`,zt=`status-active`,Bt=`url-cell`,Z=`timestamp-cell`,Vt=`actions-cell`,Ht=`kill-btn`,Ut=`empty-state`;function Wt({browsers:e}){return e.length===0?R(`div`,{class:Ft,children:z(`div`,{class:Ut,children:[R(`h3`,{children:`No Active Browsers`}),R(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):R(`div`,{class:Ft,children:z(`table`,{class:`browser-table`,children:[R(`thead`,{children:z(`tr`,{children:[R(`th`,{children:`ID`}),R(`th`,{children:`Status`}),R(`th`,{children:`URL / Title`}),R(`th`,{children:`Created`}),R(`th`,{children:`Age`}),R(`th`,{children:`Actions`})]})}),R(`tbody`,{id:`browser-table-body`,children:e.map(e=>z(_e,{children:[z(`tr`,{class:Lt,children:[R(`td`,{children:e.id}),R(`td`,{children:z(`span`,{class:zt,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),R(`td`,{children:e.profileName||`Default Profile`}),R(`td`,{class:Z,children:Mt(e.createdAt)}),R(`td`,{class:Z,children:Pt(e.createdAt)}),R(`td`,{class:Vt,children:R(`button`,{type:`button`,class:Ht,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>z(`tr`,{class:Rt,children:[z(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),R(`td`,{children:R(`span`,{class:zt,children:`Open`})}),R(`td`,{class:Bt,title:t.url,children:t.title||t.url||`about:blank`}),R(`td`,{class:Z,children:Mt(t.createdAt)}),R(`td`,{class:Z,children:Pt(t.createdAt)}),R(`td`,{class:Vt,children:R(`button`,{type:`button`,class:Ht,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function Gt({stats:e}){return z(`div`,{class:`stats-container`,children:[z(`div`,{class:It,children:[R(`h3`,{children:`Active Browsers`}),R(`div`,{class:`value`,children:e.totalBrowsers})]}),z(`div`,{class:It,children:[R(`h3`,{children:`Active Pages`}),R(`div`,{class:`value`,children:e.totalPages})]})]})}function Kt({browsers:e,stats:t}){return z(`div`,{class:`dashboard-container`,children:[z(`div`,{class:`dashboard-header`,children:[z(`div`,{children:[R(`h1`,{children:`Playwright MCP Dashboard`}),R(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),z(`div`,{children:[R(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),R(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),R(Gt,{stats:t}),R(Wt,{browsers:e}),R(`script`,{children:ge(`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agimon-ai/browse-tool",
3
3
  "description": "MCP server for browser automation using Playwright with profile management, page registry, and multi-browser support",
4
- "version": "0.15.1",
4
+ "version": "0.15.2",
5
5
  "license": "BUSL-1.1",
6
6
  "keywords": [
7
7
  "mcp",
@@ -44,10 +44,10 @@
44
44
  "ws": "8.20.0",
45
45
  "yaml": "2.8.3",
46
46
  "zod": "4.4.1",
47
- "@agimon-ai/foundation-validator": "0.12.1",
48
- "@agimon-ai/log-sink-mcp": "0.15.1",
49
- "@agimon-ai/foundation-process-registry": "0.15.1",
50
- "@agimon-ai/foundation-port-registry": "0.15.1"
47
+ "@agimon-ai/foundation-port-registry": "0.15.2",
48
+ "@agimon-ai/foundation-validator": "0.12.2",
49
+ "@agimon-ai/log-sink-mcp": "0.15.2",
50
+ "@agimon-ai/foundation-process-registry": "0.15.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/chrome": "0.1.40",