@agimon-ai/browse-tool 0.11.1 → 0.12.1
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 +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/extension/background.js +71 -28
- package/dist/extension/background.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{playwright-test-DR8tmuiB.cjs → playwright-test-D2-s0LpN.cjs} +1 -1
- package/dist/streamable-http-CJeeA9N3.mjs +53 -0
- package/dist/{streamable-http-SGOVRHb7.cjs → streamable-http-khiDB7Cd.cjs} +45 -6
- package/dist/stubs/playwright-test.cjs +1 -1
- package/package.json +5 -5
- package/dist/streamable-http-ajTIeOoU.mjs +0 -14
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const e=require(`./streamable-http-SGOVRHb7.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.E(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(`@modelcontextprotocol/sdk/server/index.js`),f=require(`@modelcontextprotocol/sdk/types.js`),p=require(`@modelcontextprotocol/sdk/server/stdio.js`),m=require(`commander`),h=require(`@hono/node-server`),g=require(`hono`),_=require(`hono/cors`),v=require(`@hono/node-ws`),y=require(`hono/html`),b=require(`hono/jsx`),x=require(`hono/jsx/jsx-runtime`);var S=`0.11.0`;const C=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function w(e,t){if(!C)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function T(t){let n=new g.Hono,r;try{r=t.get(e.T.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.T.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.T.ExtensionTaskQueue),a=t.get(e.T.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.T.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.T.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.T.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.T.PageRegistry),a=t.get(e.T.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.T.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return w(`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.T.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return w(`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:{}}}),w(`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.T.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.T.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.T.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 E(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 D=[`pnpm-workspace.yaml`,`nx.json`,`.git`],O=`browse-tool-chrome`,k=`tool`,ee=process.env.NODE_ENV||`development`,te=`127.0.0.1`;function ne(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of D)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 re(){return new t.PortRegistryService(process.env.PORT_REGISTRY_PATH)}async function ie(e,t,n){let r=re(),i=await r.reservePort({repositoryPath:e,serviceName:O,serviceType:k,environment:ee,preferredPort:t,portRange:n,pid:process.pid,host:te,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:O,serviceType:k,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 ae(){return new r.ContainerModule(t=>{t.bind(e.T.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.T.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function oe(e){let t=new d.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(f.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(f.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const se=new m.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=E(i.port),c=ne(process.cwd()),l=process.env.PORT_REGISTRY_PATH;l&&(process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(l,`processes.json`)),a=await ie(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:O,serviceType:k,environment:ee,pid:process.pid,port:u,host:te,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(ae());let f=d.get(e.T.ExtensionTaskQueue),m=d.get(e.T.ExtensionToolDelegator),v=new g.Hono;v.use(`*`,(0,_.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let y=T(d);v.route(`/extension`,y),v.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,h.serve)({fetch:v.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=oe(m),S=new p.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)}}),ce=`BROWSE_TOOL_CONFIG`,le=`.browse-tool`,ue=`config.json`,de=[`json`,`text`,`quiet`],fe={commands:{},tools:{}},pe=l.z.record(l.z.string(),l.z.unknown()),me=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(de).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),tools:l.z.object({format:l.z.enum(de).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()}),he=l.z.object({commands:me.default({}),tools:l.z.record(l.z.string(),pe).default({})});let A={config:fe};function ge(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 _e(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,ue):t}function ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),o=t.homeDir??(0,s.homedir)(),c=ge(e);if(c)return _e(c);if(n[ce])return _e(n[ce]);let l=i.default.join(r,le,ue);if((0,a.existsSync)(l))return l;let u=i.default.join(o,le,ue);if((0,a.existsSync)(u))return u}function ye(e){let t=(0,a.readFileSync)(e,`utf8`),n=JSON.parse(t);return he.parse(n)}function be(e,t={}){let n=ve(e,t);return n?{configPath:n,config:ye(n)}:{config:fe}}function xe(e,t={}){return A=be(e,t),A}function j(e){return(A.config.commands??{})[e]??{}}function Se(e){return A.config.tools?.[e]??{}}function M(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var Ce=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.T.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!t.running||!t.port)throw Error(t.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=t.port,t.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function N(e){return new Ce(e)}const we={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function P(e,t,n){return n?`${we[t]}${e}${we.reset}`:e}function F(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(P(`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(P(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
2
|
+
const e=require(`./streamable-http-khiDB7Cd.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.E(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(`@modelcontextprotocol/sdk/server/index.js`),f=require(`@modelcontextprotocol/sdk/types.js`),p=require(`@modelcontextprotocol/sdk/server/stdio.js`),m=require(`commander`),h=require(`@hono/node-server`),g=require(`hono`),_=require(`hono/cors`),v=require(`@hono/node-ws`),y=require(`hono/html`),b=require(`hono/jsx`),x=require(`hono/jsx/jsx-runtime`);var S=`0.12.0`;const C=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function w(e,t){if(!C)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function T(t){let n=new g.Hono,r;try{r=t.get(e.T.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.T.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.T.ExtensionTaskQueue),a=t.get(e.T.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.T.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.T.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.T.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.T.PageRegistry),a=t.get(e.T.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.T.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return w(`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.T.BrowserService),o=await a.persistExtensionRecordingChunk(i.browserId,i.chunkBase64);return w(`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:{}}}),w(`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.T.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.T.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.T.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 E(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 D=[`pnpm-workspace.yaml`,`nx.json`,`.git`],O=`browse-tool-chrome`,k=`tool`,ee=process.env.NODE_ENV||`development`,te=`127.0.0.1`;function ne(e=process.cwd()){let t=i.default.resolve(e);for(;;){for(let e of D)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 re(){return new t.PortRegistryService(process.env.PORT_REGISTRY_PATH)}async function ie(e,t,n){let r=re(),i=await r.reservePort({repositoryPath:e,serviceName:O,serviceType:k,environment:ee,preferredPort:t,portRange:n,pid:process.pid,host:te,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:O,serviceType:k,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 ae(){return new r.ContainerModule(t=>{t.bind(e.T.ExtensionTaskQueue).to(e.h).inSingletonScope(),t.bind(e.T.ExtensionToolDelegator).to(e.m).inSingletonScope()})}function oe(e){let t=new d.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(f.ListToolsRequestSchema,async()=>({tools:n})),t.setRequestHandler(f.CallToolRequestSchema,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const se=new m.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=E(i.port),c=ne(process.cwd()),l=process.env.PORT_REGISTRY_PATH;l&&(process.env.PROCESS_REGISTRY_PATH=(0,n.resolveSiblingRegistryPath)(l,`processes.json`)),a=await ie(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:O,serviceType:k,environment:ee,pid:process.pid,port:u,host:te,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(ae());let f=d.get(e.T.ExtensionTaskQueue),m=d.get(e.T.ExtensionToolDelegator),v=new g.Hono;v.use(`*`,(0,_.cors)({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let y=T(d);v.route(`/extension`,y),v.get(`/health`,e=>{let t=f.getConnectionStatus();return e.json({status:`healthy`,service:`browse-tool-chrome`,extension:t})});let b=(0,h.serve)({fetch:v.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=oe(m),S=new p.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)}}),ce=`BROWSE_TOOL_CONFIG`,le=`.browse-tool`,ue=`config.json`,de=[`json`,`text`,`quiet`],fe={commands:{},tools:{}},pe=l.z.record(l.z.string(),l.z.unknown()),me=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(de).optional(),color:l.z.boolean().optional(),port:l.z.coerce.number().int().positive().optional()}).partial().optional(),tools:l.z.object({format:l.z.enum(de).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()}),he=l.z.object({commands:me.default({}),tools:l.z.record(l.z.string(),pe).default({})});let A={config:fe};function ge(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 _e(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,ue):t}function ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),o=t.homeDir??(0,s.homedir)(),c=ge(e);if(c)return _e(c);if(n[ce])return _e(n[ce]);let l=i.default.join(r,le,ue);if((0,a.existsSync)(l))return l;let u=i.default.join(o,le,ue);if((0,a.existsSync)(u))return u}function ye(e){let t=(0,a.readFileSync)(e,`utf8`),n=JSON.parse(t);return he.parse(n)}function be(e,t={}){let n=ve(e,t);return n?{configPath:n,config:ye(n)}:{config:fe}}function xe(e,t={}){return A=be(e,t),A}function j(e){return(A.config.commands??{})[e]??{}}function Se(e){return A.config.tools?.[e]??{}}function M(e,t,n,r,i){let a=e.getOptionValueSourceWithGlobals(t);return a!==void 0&&a!==`default`&&a!==`implied`?n:i===void 0?r===void 0?n:r:i}var Ce=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.T.HttpServerManager).ensureRunning(this.port,{exactPort:this.exactPort});if(!t.running||!t.port)throw Error(t.error??`Failed to start HTTP server. Try running "browse-tool http-serve" manually.`);return this.serverPort=t.port,t.port}async getBaseUrl(){return`http://localhost:${await this.ensureServer()}`}async listTools(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/tools`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async listCustomTools(e){let t=await this.getBaseUrl(),n=new AbortController,r=setTimeout(()=>n.abort(),this.timeout);try{let r=new URL(`/custom-tools`,t);r.searchParams.set(`dir`,e);let i=await fetch(r,{method:`GET`,headers:{Accept:`application/json`},signal:n.signal});if(!i.ok)throw Error(`HTTP ${i.status}: ${i.statusText}`);let a=await i.json();if(a.error)throw Error(a.error);return a.tools}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(r)}}async listBrowsers(){let e=await this.getBaseUrl(),t=new AbortController,n=setTimeout(()=>t.abort(),this.timeout);try{let n=await fetch(`${e}/browsers`,{method:`GET`,headers:{Accept:`application/json`},signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);let r=await n.json();if(r.error)throw Error(r.error);return r.browsers}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(n)}}async execute(e,t){let n=await this.getBaseUrl(),r=e===`run_spec`?void 0:this.timeout,i=new AbortController,a=r===void 0?void 0:setTimeout(()=>i.abort(),r);try{let a=await fetch(`${n}/execute`,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:e,arguments:t}),signal:r===void 0?void 0:i.signal});if(!a.ok){let e=await a.text();throw Error(`HTTP ${a.status}: ${e||a.statusText}`)}let o=await a.json();return o.success?o.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:o.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{a&&clearTimeout(a)}}async executeCustomTool(e,t,n){let r=await this.getBaseUrl(),i=new AbortController,a=setTimeout(()=>i.abort(),this.timeout);try{let a=new URL(`/custom-tools`,r);a.searchParams.set(`dir`,e);let o=await fetch(a,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`application/json`},body:JSON.stringify({tool:t,arguments:n}),signal:i.signal});if(!o.ok){let e=await o.text();throw Error(`HTTP ${o.status}: ${e||o.statusText}`)}let s=await o.json();return s.success?s.result??{content:[{type:`text`,text:`No result returned`}]}:{content:[{type:`text`,text:s.error??`Unknown error`}],isError:!0}}catch(e){throw e instanceof Error&&e.name===`AbortError`?Error(`Request timeout after ${this.timeout}ms`,{cause:e}):this.wrapConnectionError(e)}finally{clearTimeout(a)}}wrapConnectionError(e){return e instanceof Error?e.message.includes(`ECONNREFUSED`)||e.message.includes(`fetch failed`)?Error(`Cannot connect to browse-tool HTTP server.\n\nTry one of the following:\n 1. Start the server: browse-tool http-serve\n 2. Check if another process is using port ${this.port}\n 3. Run with a different port: browse-tool --port 3201 <command>\n\nOriginal error: ${e.message}`,{cause:e}):e:Error(String(e),{cause:e})}};function N(e){return new Ce(e)}const we={reset:`\x1B[0m`,red:`\x1B[31m`,green:`\x1B[32m`,yellow:`\x1B[33m`,blue:`\x1B[34m`,magenta:`\x1B[35m`,cyan:`\x1B[36m`,gray:`\x1B[90m`,bold:`\x1B[1m`};function P(e,t,n){return n?`${we[t]}${e}${we.reset}`:e}function F(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(P(`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(P(`[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=P(r,`cyan`,t),a=ke(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
4
|
`)}function ke(e,t){return e===null?P(`null`,`gray`,t):e===void 0?P(`undefined`,`gray`,t):typeof e==`boolean`?P(String(e),e?`green`:`red`,t):typeof e==`number`?P(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?P(e,`blue`,t):e:Array.isArray(e)?e.length===0?P(`[]`,`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 P(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function I(e,t){return P(`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=P(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
5
|
`)}function Me(e,t){let n=j(`tools`),r=M(e,`format`,t.format,n.format),i=M(e,`color`,t.color,n.color);return{port:M(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const Ne=new m.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 N({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(I(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),Pe=new m.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(I(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await N({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=F(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(I(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new m.Command(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(Ne).addCommand(Pe);const Fe=new m.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.w).option(`--image <image>`,`Docker image tag to produce`,e.S).option(`--platform <platform>`,`Docker target platform`,e.C).action(async t=>{try{let n=await e.y({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.b(n.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),Ie=new m.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=j(`exec`),i=M(this,`format`,n.format,r.format),a=M(this,`color`,n.color,r.color),o=M(this,`port`,n.port,r.port===void 0?void 0:String(r.port),process.env.PLAYWRIGHT_PORT),s=this.getOptionValueSourceWithGlobals(`port`),c=s!==void 0&&s!==`default`&&s!==`implied`,l={format:i,color:a};try{let n;try{n=JSON.parse(t)}catch{console.error(I(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await N({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=F(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(I(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),L=`pageId`,R=`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 z(e,t){if(Ve){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function B(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ue(e){return B(e)&&Array.isArray(e.content)}function V(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 H(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]=H(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=U(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]=H(e,i+1,r.indent);s[t]=a,i=o}else s[t]=V(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]=H(e,i,t.indent);if(!B(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=U(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]=H(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=V(a),i+=1}r.push(s);continue}r.push(V(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]=U(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]=H(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=V(o),i+=1}return[r,i]}function U(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 Je(e){let t=Ge(e);if(t.length===0)return{};let[n]=H(t,0,t[0].indent);return n}function Ye(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!B(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[L],r=t[R];if(n!==void 0&&!(B(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(B(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[L]={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[R]={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 Xe(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 Ze(e,t){let n=typeof t.run==`function`?t.run:void 0;if(!n)throw Error(`Custom tool script "${e}" must export a "run" function`);return{execute:n}}function Qe(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function $e(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(B(r)){let i=Qe(r,t);return{...e,content:[{...n,text:JSON.stringify(i,null,2)},...e.content.slice(1)]}}}catch{}return{...e,content:[...e.content,{type:`text`,text:`suggestionActions: ${t}`}]}}function et(e,t,n){if(Ue(t))return n?$e(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=B(t)&&n?Qe(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var tt=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(e,t){let n=typeof t[L]==`string`&&t[L].length>0?t[L]:void 0,r=typeof t[R]==`string`&&t[R].length>0?t[R]:void 0;if(!n&&!r)throw Error(`Custom tool "${e}" requires a string pageId or browserId`);if(n){let t=this.pageRegistry.get(n);if(!t)throw Error(`Page "${n}" not found`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}let i=this.createBrowserHelper(e,r);try{let e=await i.getCurrentPage(),t=this.pageRegistry.get(e.pageId);if(!t)throw Error(`Page "${e.pageId}" not found`);return{pageId:e.pageId,pageEntry:t,page:e.page,browser:i}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await i.newPage(),n=this.pageRegistry.get(t.pageId);if(!n)throw Error(`Page "${t.pageId}" not found`,{cause:e});return{pageId:t.pageId,pageEntry:n,page:t.page,browser:i}}}createBrowserHelper(e,t){return{browserId:t,mode:this.browserService?.getBrowser(t)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(t);if(!e)throw Error(`Browser "${t}" not found`);return this.pageRegistry.findByBrowser(t).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async n=>{let r=this.pageRegistry.get(n);if(!r||r.browserId!==t)throw Error(`Page "${n}" not found in browser "${t}"`);let i=this.browserService?.getBrowser(t);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(e,n,r)}},getCurrentPage:async()=>{let n=this.browserService?.getBrowser(t);if(!n)throw Error(`Browser "${t}" not found`);let r=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},newPage:async n=>this.createPageForBrowser(e,t,n)}}createToolLogger(e,t,n){let r={"browse_tool.tool.name":e,"browse_tool.page.id":t,"browse_tool.browser.id":n.browserId,"browse_tool.execution.mode":n.mode},i=(e,t,n)=>{this.telemetry.log(e,t,{attributes: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[L]==`string`?n[L]:void 0,a=typeof n[R]==`string`?n[R]: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}),z(`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 z(`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})}z(`Custom tool execution completed`,{toolName:t,pageId:o,browserId:s.browserId,mode:s.mode,result:He(f)});let p=et(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=Je(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=>{Ye(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=Ze(n,await Xe(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function nt(t){let n=new g.Hono;return n.get(`/browsers`,async n=>{try{let r=t.get(e.T.BrowserService),i=t.get(e.T.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.T.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.T.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.T.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 rt(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 it(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 at(e){return it(new Date().getTime()-e.getTime())}const ot=`table-container`,st=`stat-card`,ct=`browser-row`,lt=`page-row`,W=`status-active`,ut=`url-cell`,G=`timestamp-cell`,K=`actions-cell`,q=`kill-btn`,dt=`empty-state`;function ft({browsers:e}){return e.length===0?(0,x.jsx)(`div`,{class:ot,children:(0,x.jsxs)(`div`,{class:dt,children:[(0,x.jsx)(`h3`,{children:`No Active Browsers`}),(0,x.jsx)(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):(0,x.jsx)(`div`,{class:ot,children:(0,x.jsxs)(`table`,{class:`browser-table`,children:[(0,x.jsx)(`thead`,{children:(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{children:`ID`}),(0,x.jsx)(`th`,{children:`Status`}),(0,x.jsx)(`th`,{children:`URL / Title`}),(0,x.jsx)(`th`,{children:`Created`}),(0,x.jsx)(`th`,{children:`Age`}),(0,x.jsx)(`th`,{children:`Actions`})]})}),(0,x.jsx)(`tbody`,{id:`browser-table-body`,children:e.map(e=>(0,x.jsxs)(b.Fragment,{children:[(0,x.jsxs)(`tr`,{class:ct,children:[(0,x.jsx)(`td`,{children:e.id}),(0,x.jsx)(`td`,{children:(0,x.jsxs)(`span`,{class:W,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),(0,x.jsx)(`td`,{children:e.profileName||`Default Profile`}),(0,x.jsx)(`td`,{class:G,children:rt(e.createdAt)}),(0,x.jsx)(`td`,{class:G,children:at(e.createdAt)}),(0,x.jsx)(`td`,{class:K,children:(0,x.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>(0,x.jsxs)(`tr`,{class:lt,children:[(0,x.jsxs)(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),(0,x.jsx)(`td`,{children:(0,x.jsx)(`span`,{class:W,children:`Open`})}),(0,x.jsx)(`td`,{class:ut,title:t.url,children:t.title||t.url||`about:blank`}),(0,x.jsx)(`td`,{class:G,children:rt(t.createdAt)}),(0,x.jsx)(`td`,{class:G,children:at(t.createdAt)}),(0,x.jsx)(`td`,{class:K,children:(0,x.jsx)(`button`,{type:`button`,class:q,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function pt({stats:e}){return(0,x.jsxs)(`div`,{class:`stats-container`,children:[(0,x.jsxs)(`div`,{class:st,children:[(0,x.jsx)(`h3`,{children:`Active Browsers`}),(0,x.jsx)(`div`,{class:`value`,children:e.totalBrowsers})]}),(0,x.jsxs)(`div`,{class:st,children:[(0,x.jsx)(`h3`,{children:`Active Pages`}),(0,x.jsx)(`div`,{class:`value`,children:e.totalPages})]})]})}function mt({browsers:e,stats:t}){return(0,x.jsxs)(`div`,{class:`dashboard-container`,children:[(0,x.jsxs)(`div`,{class:`dashboard-header`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h1`,{children:`Playwright MCP Dashboard`}),(0,x.jsx)(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),(0,x.jsx)(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),(0,x.jsx)(pt,{stats:t}),(0,x.jsx)(ft,{browsers:e}),(0,x.jsx)(`script`,{children:(0,y.raw)(`
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{C as e,S as t,T as n,_ as r,a as i,b as a,c as o,d as s,f as c,g as l,h as u,i as d,l as f,m as p,n as m,o as h,p as g,s as _,t as v,u as y,v as b,w as x,x as S,y as C}from"./streamable-http-ajTIeOoU.mjs";import{stripTypeScriptTypes as w}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as T,PortRegistryService as E}from"@agimon-ai/foundation-port-registry";import{createProcessLease as ee,resolveSiblingRegistryPath as te}from"@agimon-ai/foundation-process-registry";import{Container as D,ContainerModule as O}from"inversify";import k from"node:path";import{existsSync as A,readFileSync as j,statSync as ne}from"node:fs";import{readFile as M,stat as re}from"node:fs/promises";import{homedir as ie}from"node:os";import{z as N}from"zod";import{SpanStatusCode as P}from"@opentelemetry/api";import{Server as ae}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as oe,ListToolsRequestSchema as se}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as ce}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as F,Option as le}from"commander";import{serve as ue}from"@hono/node-server";import{Hono as I}from"hono";import{cors as de}from"hono/cors";import{createNodeWebSocket as fe}from"@hono/node-ws";import{raw as pe}from"hono/html";import{Fragment as me}from"hono/jsx";import{jsx as L,jsxs as R}from"hono/jsx/jsx-runtime";var he=`0.11.0`;const ge=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function _e(e,t){if(!ge)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function ve(e){let t=new I,i;try{i=e.get(n.TelemetryService)}catch{i=new l}return t.get(`/telemetry-config`,e=>{try{let t=new URL(e.req.url).origin;return e.json(r(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 r=e.get(n.ExtensionTaskQueue).getNextTask();return r?t.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let r=await t.req.json();if(!r.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(n.ExtensionTaskQueue),a=e.get(n.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),t.json({success:!0})):t.json({success:!1,error:`Task ${r.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 r=e.get(n.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).register(r);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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).heartbeat(r);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 ${r.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 r=await t.req.json();if(!r.pageId||typeof r.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(n.PageRegistry),a=e.get(n.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return _e(`artifact received`,{browserId:r.browserId,videoBase64Size:r.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(r.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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 r=await t.req.json();if(!r.browserId||!r.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(n.BrowserService),o=await a.persistExtensionRecordingChunk(r.browserId,r.chunkBase64);return _e(`chunk received`,{browserId:r.browserId,chunkIndex:r.chunkIndex,mimeType:r.mimeType,persisted:!!o,chunkBase64Size:r.chunkBase64.length}),o?(i.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":r.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof r.chunkIndex==`number`?{"browse_tool.recording.chunk_index":r.chunkIndex}:{},...typeof r.mimeType==`string`?{"browse_tool.recording.mime_type":r.mimeType}:{}}}),a.recordBrowserActivity(r.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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):(i.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),_e(`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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).requestHandoff(r);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 ${r.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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).acknowledgeHandoff(r.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 ${r.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 r=e.get(n.ExtensionSessionRegistry).listSessions();return t.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 t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function ye(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 be=[`pnpm-workspace.yaml`,`nx.json`,`.git`],xe=`browse-tool-chrome`,Se=`tool`,Ce=process.env.NODE_ENV||`development`,we=`127.0.0.1`;function Te(e=process.cwd()){let t=k.resolve(e);for(;;){for(let e of be)if(A(k.join(t,e)))return t;let e=k.dirname(t);if(e===t)return process.cwd();t=e}}function Ee(){return new E(process.env.PORT_REGISTRY_PATH)}async function De(e,t,n){let r=Ee(),i=await r.reservePort({repositoryPath:e,serviceName:xe,serviceType:Se,environment:Ce,preferredPort:t,portRange:n,pid:process.pid,host:we,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:xe,serviceType:Se,environment:Ce,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function Oe(){return new O(e=>{e.bind(n.ExtensionTaskQueue).to(u).inSingletonScope(),e.bind(n.ExtensionToolDelegator).to(p).inSingletonScope()})}function ke(e){let t=new ae({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(se,async()=>({tools:n})),t.setRequestHandler(oe,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ae=new F(`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,r;try{let i=ye(e.port),a=Te(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=te(o,`processes.json`)),t=await De(a,i??T.min,i?{min:i,max:i}:T);let s=t.port;r=await ee({repositoryPath:a,serviceName:xe,serviceType:Se,environment:Ce,pid:process.pid,port:s,host:we,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 D({defaultScope:`Singleton`});c.load(Oe());let l=c.get(n.ExtensionTaskQueue),u=c.get(n.ExtensionToolDelegator),d=new I;d.use(`*`,de({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=ve(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=ue({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=ke(u),h=new ce;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 r.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(r||t)try{await r?.release(),await t?.release()}catch{}let n=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${n}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),je=`BROWSE_TOOL_CONFIG`,Me=`.browse-tool`,Ne=`config.json`,Pe=[`json`,`text`,`quiet`],Fe={commands:{},tools:{}},Ie=N.record(N.string(),N.unknown()),Le=N.object({mcpServe:N.object({type:N.string().optional(),browser:N.string().optional(),headless:N.boolean().optional(),profile:N.string().optional(),mode:N.string().optional(),host:N.string().optional(),port:N.coerce.number().int().positive().optional(),httpPort:N.coerce.number().int().positive().optional(),tags:N.string().optional(),exclude:N.string().optional(),customTools:N.string().optional(),chromeForTestingPath:N.string().optional(),snippetsDir:N.string().optional(),registryPath:N.string().optional(),registryDir:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),httpServe:N.object({port:N.coerce.number().int().positive().optional(),headless:N.boolean().optional(),idleTimeout:N.coerce.number().positive().optional(),host:N.string().optional(),registryDir:N.string().optional(),registryPath:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),snippetsDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),exec:N.object({format:N.enum(Pe).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),tools:N.object({format:N.enum(Pe).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),status:N.record(N.string(),N.unknown()).optional(),stop:N.record(N.string(),N.unknown()).optional()}),Re=N.object({commands:Le.default({}),tools:N.record(N.string(),Ie).default({})});let z={config:Fe};function ze(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 Be(e){let t=k.resolve(e);if(!A(t))throw Error(`Config path not found: ${t}`);return ne(t).isDirectory()?k.join(t,Ne):t}function Ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??ie(),a=ze(e);if(a)return Be(a);if(n[je])return Be(n[je]);let o=k.join(r,Me,Ne);if(A(o))return o;let s=k.join(i,Me,Ne);if(A(s))return s}function He(e){let t=j(e,`utf8`),n=JSON.parse(t);return Re.parse(n)}function Ue(e,t={}){let n=Ve(e,t);return n?{configPath:n,config:He(n)}:{config:Fe}}function We(e,t={}){return z=Ue(e,t),z}function B(e){return(z.config.commands??{})[e]??{}}function Ge(e){return z.config.tools?.[e]??{}}function V(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 Ke=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??s,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await h().get(n.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 H(e){return new Ke(e)}const qe={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 U(e,t,n){return n?`${qe[t]}${e}${qe.reset}`:e}function Je(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Ye(e):Xe(e,r)}function Ye(e){return JSON.stringify(e,null,2)}function Xe(e,t){let n=[];e.isError&&n.push(U(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Ze(r,t)):r.type===`image`?n.push(et(r,t)):n.push(U(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
2
|
+
import{C as e,S as t,T as n,_ as r,a as i,b as a,c as o,d as s,f as c,g as l,h as u,i as d,l as f,m as p,n as m,o as h,p as g,s as _,t as v,u as y,v as b,w as x,x as S,y as C}from"./streamable-http-CJeeA9N3.mjs";import{stripTypeScriptTypes as w}from"node:module";import"reflect-metadata/lite";import{DEFAULT_PORT_RANGE as T,PortRegistryService as E}from"@agimon-ai/foundation-port-registry";import{createProcessLease as ee,resolveSiblingRegistryPath as te}from"@agimon-ai/foundation-process-registry";import{Container as D,ContainerModule as O}from"inversify";import k from"node:path";import{existsSync as A,readFileSync as j,statSync as ne}from"node:fs";import{readFile as M,stat as re}from"node:fs/promises";import{homedir as ie}from"node:os";import{z as N}from"zod";import{SpanStatusCode as P}from"@opentelemetry/api";import{Server as ae}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as oe,ListToolsRequestSchema as se}from"@modelcontextprotocol/sdk/types.js";import{StdioServerTransport as ce}from"@modelcontextprotocol/sdk/server/stdio.js";import{Command as F,Option as le}from"commander";import{serve as ue}from"@hono/node-server";import{Hono as I}from"hono";import{cors as de}from"hono/cors";import{createNodeWebSocket as fe}from"@hono/node-ws";import{raw as pe}from"hono/html";import{Fragment as me}from"hono/jsx";import{jsx as L,jsxs as R}from"hono/jsx/jsx-runtime";var he=`0.12.0`;const ge=process.env.BROWSE_TOOL_DEBUG_EXTENSION_RECORDING===`1`;function _e(e,t){if(!ge)return;let n=t?` ${JSON.stringify(t)}`:``;console.log(`[ExtensionRecordingDebug] ${e}${n}`)}function ve(e){let t=new I,i;try{i=e.get(n.TelemetryService)}catch{i=new l}return t.get(`/telemetry-config`,e=>{try{let t=new URL(e.req.url).origin;return e.json(r(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 r=e.get(n.ExtensionTaskQueue).getNextTask();return r?t.json({task:{id:r.id,tool:r.tool,arguments:r.arguments,telemetry:r.telemetry}}):t.json({})}catch(e){return t.json({error:e instanceof Error?e.message:String(e)},500)}}),t.post(`/result`,async t=>{try{let r=await t.req.json();if(!r.taskId)return t.json({success:!1,error:`Missing taskId in request body`},400);let i=e.get(n.ExtensionTaskQueue),a=e.get(n.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),t.json({success:!0})):t.json({success:!1,error:`Task ${r.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 r=e.get(n.ExtensionTaskQueue),i=r.getConnectionStatus(),a={connected:i.connected,lastPollAt:i.lastPollAt?.toISOString(),lastResultAt:i.lastResultAt?.toISOString(),pendingTasks:i.pendingTasks,queueSize:r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).register(r);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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).heartbeat(r);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 ${r.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 r=await t.req.json();if(!r.pageId||typeof r.tabId!=`number`)return t.json({success:!1,error:`Missing pageId or tabId in request body`},400);let i=e.get(n.PageRegistry),a=e.get(n.BrowserService),o=i.get(r.pageId);return o?(o.extensionTabId=r.tabId,a.recordBrowserActivity(o.browserId,r.pageId),t.json({success:!0})):t.json({success:!1,error:`Page ${r.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 r=await t.req.json();if(!r.browserId)return t.json({success:!1,error:`Missing browserId in request body`},400);let i=e.get(n.BrowserService),a=await i.persistExtensionRecordingArtifact(r.browserId,r.videoBase64);return _e(`artifact received`,{browserId:r.browserId,videoBase64Size:r.videoBase64?.length??0,persisted:a}),a?(i.recordBrowserActivity(r.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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 r=await t.req.json();if(!r.browserId||!r.chunkBase64)return t.json({success:!1,error:`Missing browserId or chunkBase64 in request body`},400);let a=e.get(n.BrowserService),o=await a.persistExtensionRecordingChunk(r.browserId,r.chunkBase64);return _e(`chunk received`,{browserId:r.browserId,chunkIndex:r.chunkIndex,mimeType:r.mimeType,persisted:!!o,chunkBase64Size:r.chunkBase64.length}),o?(i.log(`debug`,`extension recording chunk received`,{attributes:{"browse_tool.extension.recording.chunk_received":!0,"browse_tool.browser.id":r.browserId,"browse_tool.recording.chunk_bytes":o.chunkBytes,"browse_tool.recording.total_bytes":o.totalBytes,"browse_tool.recording.chunk_count":o.chunkCount,...typeof r.chunkIndex==`number`?{"browse_tool.recording.chunk_index":r.chunkIndex}:{},...typeof r.mimeType==`string`?{"browse_tool.recording.mime_type":r.mimeType}:{}}}),a.recordBrowserActivity(r.browserId),t.json({success:!0})):t.json({success:!1,error:`Browser "${r.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):(i.log(t.level??`info`,t.message,{attributes:{"browse_tool.extension.log_relay":!0,...typeof t.attributes==`object`&&t.attributes!==null?t.attributes:{}}}),_e(`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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).requestHandoff(r);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 ${r.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 r=await t.req.json();if(!r.sessionId)return t.json({success:!1,error:`Missing sessionId in request body`},400);let i=e.get(n.ExtensionSessionRegistry).acknowledgeHandoff(r.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 ${r.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 r=e.get(n.ExtensionSessionRegistry).listSessions();return t.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 t.json({error:e instanceof Error?e.message:String(e)},500)}}),t}function ye(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 be=[`pnpm-workspace.yaml`,`nx.json`,`.git`],xe=`browse-tool-chrome`,Se=`tool`,Ce=process.env.NODE_ENV||`development`,we=`127.0.0.1`;function Te(e=process.cwd()){let t=k.resolve(e);for(;;){for(let e of be)if(A(k.join(t,e)))return t;let e=k.dirname(t);if(e===t)return process.cwd();t=e}}function Ee(){return new E(process.env.PORT_REGISTRY_PATH)}async function De(e,t,n){let r=Ee(),i=await r.reservePort({repositoryPath:e,serviceName:xe,serviceType:Se,environment:Ce,preferredPort:t,portRange:n,pid:process.pid,host:we,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:xe,serviceType:Se,environment:Ce,pid:process.pid});if(!n.success&&!n.error?.includes(`No matching registry entry`))throw Error(n.error||`Failed to release port ${t}`)}}}function Oe(){return new O(e=>{e.bind(n.ExtensionTaskQueue).to(u).inSingletonScope(),e.bind(n.ExtensionToolDelegator).to(p).inSingletonScope()})}function ke(e){let t=new ae({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(se,async()=>({tools:n})),t.setRequestHandler(oe,async t=>{let{name:n,arguments:r}=t.params;return await e.executeTool(n,r||{})}),t}const Ae=new F(`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,r;try{let i=ye(e.port),a=Te(process.cwd()),o=process.env.PORT_REGISTRY_PATH;o&&(process.env.PROCESS_REGISTRY_PATH=te(o,`processes.json`)),t=await De(a,i??T.min,i?{min:i,max:i}:T);let s=t.port;r=await ee({repositoryPath:a,serviceName:xe,serviceType:Se,environment:Ce,pid:process.pid,port:s,host:we,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 D({defaultScope:`Singleton`});c.load(Oe());let l=c.get(n.ExtensionTaskQueue),u=c.get(n.ExtensionToolDelegator),d=new I;d.use(`*`,de({origin:e=>e??null,credentials:!0,allowMethods:[`GET`,`POST`,`OPTIONS`],allowHeaders:[`Content-Type`,`Accept`]}));let f=ve(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=ue({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=ke(u),h=new ce;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 r.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(r||t)try{await r?.release(),await t?.release()}catch{}let n=e instanceof Error?e.message:String(e);console.error(`Error [SERVER_ERROR]: Failed to start Chrome extension MCP server: ${n}`),console.error(`Recovery: Check that the port is available and try again.`),process.exit(1)}}),je=`BROWSE_TOOL_CONFIG`,Me=`.browse-tool`,Ne=`config.json`,Pe=[`json`,`text`,`quiet`],Fe={commands:{},tools:{}},Ie=N.record(N.string(),N.unknown()),Le=N.object({mcpServe:N.object({type:N.string().optional(),browser:N.string().optional(),headless:N.boolean().optional(),profile:N.string().optional(),mode:N.string().optional(),host:N.string().optional(),port:N.coerce.number().int().positive().optional(),httpPort:N.coerce.number().int().positive().optional(),tags:N.string().optional(),exclude:N.string().optional(),customTools:N.string().optional(),chromeForTestingPath:N.string().optional(),snippetsDir:N.string().optional(),registryPath:N.string().optional(),registryDir:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),httpServe:N.object({port:N.coerce.number().int().positive().optional(),headless:N.boolean().optional(),idleTimeout:N.coerce.number().positive().optional(),host:N.string().optional(),registryDir:N.string().optional(),registryPath:N.string().optional(),pidsDir:N.string().optional(),profilesDir:N.string().optional(),snippetsDir:N.string().optional(),proxyConfigDir:N.string().optional()}).partial().optional(),exec:N.object({format:N.enum(Pe).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),tools:N.object({format:N.enum(Pe).optional(),color:N.boolean().optional(),port:N.coerce.number().int().positive().optional()}).partial().optional(),status:N.record(N.string(),N.unknown()).optional(),stop:N.record(N.string(),N.unknown()).optional()}),Re=N.object({commands:Le.default({}),tools:N.record(N.string(),Ie).default({})});let z={config:Fe};function ze(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 Be(e){let t=k.resolve(e);if(!A(t))throw Error(`Config path not found: ${t}`);return ne(t).isDirectory()?k.join(t,Ne):t}function Ve(e,t){let n=t.env??process.env,r=t.cwd??process.cwd(),i=t.homeDir??ie(),a=ze(e);if(a)return Be(a);if(n[je])return Be(n[je]);let o=k.join(r,Me,Ne);if(A(o))return o;let s=k.join(i,Me,Ne);if(A(s))return s}function He(e){let t=j(e,`utf8`),n=JSON.parse(t);return Re.parse(n)}function Ue(e,t={}){let n=Ve(e,t);return n?{configPath:n,config:He(n)}:{config:Fe}}function We(e,t={}){return z=Ue(e,t),z}function B(e){return(z.config.commands??{})[e]??{}}function Ge(e){return z.config.tools?.[e]??{}}function V(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 Ke=class{port;exactPort;timeout;serverPort=null;constructor(e={}){this.port=e.port??s,this.exactPort=e.exactPort??!1,this.timeout=e.timeout??6e4}async ensureServer(){if(this.serverPort!==null)return this.serverPort;let e=await h().get(n.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 H(e){return new Ke(e)}const qe={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 U(e,t,n){return n?`${qe[t]}${e}${qe.reset}`:e}function Je(e,t){let{format:n,color:r}=t;return n===`quiet`?``:n===`json`?Ye(e):Xe(e,r)}function Ye(e){return JSON.stringify(e,null,2)}function Xe(e,t){let n=[];e.isError&&n.push(U(`Error:`,`red`,t));for(let r of e.content)r.type===`text`?n.push(Ze(r,t)):r.type===`image`?n.push(et(r,t)):n.push(U(`[Unknown content type: ${r.type}]`,`yellow`,t));return n.join(`
|
|
3
3
|
`)}function Ze(e,t){let n=e.text;try{return Qe(JSON.parse(n),t)}catch{return n}}function Qe(e,t){if(typeof e!=`object`||!e)return String(e);let n=[];for(let[r,i]of Object.entries(e)){let e=U(r,`cyan`,t),a=$e(i,t);n.push(`${e}: ${a}`)}return n.join(`
|
|
4
4
|
`)}function $e(e,t){return e===null?U(`null`,`gray`,t):e===void 0?U(`undefined`,`gray`,t):typeof e==`boolean`?U(String(e),e?`green`:`red`,t):typeof e==`number`?U(String(e),`yellow`,t):typeof e==`string`?e.startsWith(`http://`)||e.startsWith(`https://`)?U(e,`blue`,t):e:Array.isArray(e)?e.length===0?U(`[]`,`gray`,t):JSON.stringify(e,null,2):typeof e==`object`?JSON.stringify(e,null,2):String(e)}function et(e,t){let{mimeType:n,data:r}=e;return U(`[Image: ${n}, ~${Math.round(r.length*3/4/1024)}KB base64 data]`,`magenta`,t)}function W(e,t){return U(`Error: ${e instanceof Error?e.message:e}`,`red`,t)}function tt(e,t){let n=Math.max(...e.map(e=>e.name.length)),r=[];for(let i of e){let e=U(i.name.padEnd(n),`cyan`,t);r.push(` ${e} ${i.description}`)}return r.join(`
|
|
5
5
|
`)}function nt(e,t){let n=B(`tools`),r=V(e,`format`,t.format,n.format),i=V(e,`color`,t.color,n.color);return{port:V(e,`port`,t.port,n.port===void 0?void 0:String(n.port),process.env.PLAYWRIGHT_PORT),formatterOptions:{format:r,color:i}}}const rt=new F(`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(s)).action(async function(e,t){let{port:n,formatterOptions:r}=nt(this,t);try{let t=await H({port:Number.parseInt(n,10)}).listCustomTools(e),i=r.format===`text`?tt(t,r.color):r.format===`quiet`?``:JSON.stringify(t,null,2);i&&console.log(i)}catch(e){console.error(W(e instanceof Error?e:String(e),r.color)),process.exit(1)}}),it=new F(`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(s)).action(async function(e,t,n,r){let{port:i,formatterOptions:a}=nt(this,r);try{let r;try{r=JSON.parse(n)}catch{console.error(W(`Invalid JSON arguments: ${n}`,a.color)),process.exit(1);return}let o=await H({port:Number.parseInt(i,10)}).executeCustomTool(e,t,r),s=Je(o,a);s&&console.log(s),o.isError&&process.exit(1)}catch(e){console.error(W(e instanceof Error?e:String(e),a.color)),process.exit(1)}});new F(`custom-tools`).description(`List and execute custom tools through the HTTP server`).addCommand(rt).addCommand(it);const at=new F(`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`,x).option(`--image <image>`,`Docker image tag to produce`,t).option(`--platform <platform>`,`Docker target platform`,e).action(async e=>{try{let t=await C({version:e.cftVersion,image:e.image,platform:e.platform,stdio:`inherit`});console.log(`Built Docker image ${t.image}`),console.log(` Version: ${t.version}`),console.log(` Platform: ${t.platform}`),console.log(` Archive: ${a(t.platform)}`)}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exit(1)}}),ot=new F(`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(s)).action(async function(e,t,n){let r=B(`exec`),i=V(this,`format`,n.format,r.format),a=V(this,`color`,n.color,r.color),o=V(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(W(`Invalid JSON arguments: ${t}`,l.color)),process.exit(1)}let r=await H({port:Number.parseInt(o,10),exactPort:c}).execute(e,n),i=Je(r,l);i&&console.log(i),r.isError&&process.exit(1)}catch(e){console.error(W(e instanceof Error?e:String(e),l.color)),process.exit(1)}}),G=`pageId`,K=`browserId`,st=N.record(N.string(),N.unknown()),ct=N.object({type:N.literal(`object`),properties:N.record(N.string(),N.unknown()).optional(),required:N.array(N.string()).optional(),additionalProperties:N.boolean().optional()}).passthrough(),lt=N.object({name:N.string().min(1),description:N.string().min(1),script:N.string().min(1),suggestionActions:N.string().min(1).optional(),capabilities:st,inputSchema:ct}),ut=N.object({tools:N.array(lt)}),dt=process.env.BROWSE_TOOL_DEBUG_CUSTOM_TOOLS===`1`;function ft(e){return JSON.stringify(e,(e,t)=>typeof t==`string`&&t.length>240?`${t.slice(0,240)}...<trimmed>`:t)}function pt(e,t){if(dt){if(t){console.error(`[CustomToolService] ${e}`,t);return}console.error(`[CustomToolService] ${e}`)}}function q(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function mt(e){return q(e)&&Array.isArray(e.content)}function J(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 ht(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 gt(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 Y(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(`-`)?_t(e,t,n):vt(e,t,n)}function _t(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]=Y(e,i+1,t.indent);r.push(a),i=o;continue}if(a.includes(`:`)){let[t,o]=yt(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]=Y(e,i+1,r.indent);s[t]=a,i=o}else s[t]=J(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]=Y(e,i,t.indent);if(!q(n))throw Error(`Expected object entry in tools.yaml at line ${i+1}`);Object.assign(s,n),i=r;continue}let[r,a]=yt(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]=Y(e,i+1,n.indent);s[r]=a,i=o;continue}s[r]=J(a),i+=1}r.push(s);continue}r.push(J(a)),i+=1}return[r,i]}function vt(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]=yt(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]=Y(e,i+1,t.indent);r[a]=o,i=s;continue}r[a]=J(o),i+=1}return[r,i]}function yt(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 bt(e){let t=gt(e);if(t.length===0)return{};let[n]=Y(t,0,t[0].indent);return n}function xt(e){e.inputSchema.properties===void 0&&(e.inputSchema.properties={});let t=e.inputSchema.properties;if(!q(t))throw Error(`Custom tool "${e.name}" inputSchema.properties must be an object`);let n=t[G],r=t[K];if(n!==void 0&&!(q(n)&&n.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.pageId as type "string"`);if(r!==void 0&&!(q(r)&&r.type===`string`))throw Error(`Custom tool "${e.name}" must define inputSchema.properties.browserId as type "string"`);n===void 0&&(t[G]={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[K]={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 St(e){let t=w(await M(e,`utf8`),{mode:`strip`});return await import(`data:text/javascript;base64,${Buffer.from(t,`utf8`).toString(`base64`)}`)}function Ct(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 wt(e,t){return typeof e.suggestionActions==`string`&&e.suggestionActions.trim().length>0?e:{...e,suggestionActions:t}}function Tt(e,t){let n=e.content[0];if(n?.type===`text`&&typeof n.text==`string`)try{let r=JSON.parse(n.text);if(q(r)){let i=wt(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 Et(e,t,n){if(mt(t))return n?Tt(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=q(t)&&n?wt(t,n):t;return{content:[{type:`text`,text:JSON.stringify(r,null,2)}]}}var Dt=class{constructor(e,t,n=new l,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 b(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[G]==`string`&&t[G].length>0?t[G]:void 0,r=typeof t[K]==`string`&&t[K].length>0?t[K]:void 0;if(!n&&!r)throw Error(`Custom tool "${e}" requires a string pageId or browserId`);if(n){let t=this.pageRegistry.get(n);if(!t)throw Error(`Page "${n}" not found`);if(r&&t.browserId!==r)throw Error(`Custom tool "${e}" received pageId "${n}" for browser "${t.browserId}", not "${r}"`);let i=this.createBrowserHelper(e,t.browserId);return{pageId:n,pageEntry:t,page:this.resolveToolPage(e,n,t),browser:i}}let i=this.createBrowserHelper(e,r);try{let e=await i.getCurrentPage(),t=this.pageRegistry.get(e.pageId);if(!t)throw Error(`Page "${e.pageId}" not found`);return{pageId:e.pageId,pageEntry:t,page:e.page,browser:i}}catch(e){if(!(e instanceof Error?e.message:String(e)).includes(`has no pages`))throw e;let t=await i.newPage(),n=this.pageRegistry.get(t.pageId);if(!n)throw Error(`Page "${t.pageId}" not found`,{cause:e});return{pageId:t.pageId,pageEntry:n,page:t.page,browser:i}}}createBrowserHelper(e,t){return{browserId:t,mode:this.browserService?.getBrowser(t)?.mode??`extension`,listPages:async()=>{let e=this.browserService?.getBrowser(t);if(!e)throw Error(`Browser "${t}" not found`);return this.pageRegistry.findByBrowser(t).map(t=>this.toPageSummary(e.currentPageId,t))},getPage:async n=>{let r=this.pageRegistry.get(n);if(!r||r.browserId!==t)throw Error(`Page "${n}" not found in browser "${t}"`);let i=this.browserService?.getBrowser(t);return{...this.toPageSummary(i?.currentPageId??null,r),page:this.resolveToolPage(e,n,r)}},getCurrentPage:async()=>{let n=this.browserService?.getBrowser(t);if(!n)throw Error(`Browser "${t}" not found`);let r=n.currentPageId??this.pageRegistry.findByBrowser(t)[0]?.id??void 0;if(!r)throw Error(`Browser "${t}" has no pages`);let i=this.pageRegistry.get(r);if(!i)throw Error(`Page "${r}" not found`);return{...this.toPageSummary(n.currentPageId,i),page:this.resolveToolPage(e,r,i)}},newPage:async n=>this.createPageForBrowser(e,t,n)}}createToolLogger(e,t,n){let r={"browse_tool.tool.name":e,"browse_tool.page.id":t,"browse_tool.browser.id":n.browserId,"browse_tool.execution.mode":n.mode},i=(e,t,n)=>{this.telemetry.log(e,t,{attributes:ht({...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[G]==`string`?n[G]:void 0,i=typeof n[K]==`string`?n[K]:void 0;return this.telemetry.runInSpan(`browse_tool.custom_tool.execute`,{attributes:{"browse_tool.tool.name":t,"browse_tool.custom_tools.directory":k.resolve(e),"browse_tool.page.id":r,"browse_tool.browser.id":i}},async r=>{let i=(await this.loadTools(e)).find(e=>e.name===t);if(!i)throw r?.setStatus({code:P.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}),pt(`Executing custom tool`,{toolName:t,directory:k.resolve(e),pageId:a,browserId:o.browserId,mode:o.mode,input:ft(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 pt(`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})}pt(`Custom tool execution completed`,{toolName:t,pageId:a,browserId:o.browserId,mode:o.mode,result:ft(u)});let d=Et(t,u,i.suggestionActions),f=d.isError?d.content[0]?.text:void 0;return f&&r?.setStatus({code:P.ERROR,message:f}),d})}async loadTools(e){let t=k.resolve(e),n=k.join(t,`tools.yaml`),r=bt(await M(n,`utf8`).catch(e=>{throw Error(`Failed to read custom tool manifest at "${n}": ${e instanceof Error?e.message:String(e)}`)})),i=ut.parse(r);return Promise.all(i.tools.map(async e=>{xt(e);let n=k.resolve(t,e.script);await re(n).catch(t=>{throw Error(`Custom tool "${e.name}" script not found at "${n}": ${t instanceof Error?t.message:String(t)}`)});let r=Ct(n,await St(n));return{name:e.name,description:e.description,suggestionActions:e.suggestionActions,inputSchema:e.inputSchema,capabilities:e.capabilities,scriptPath:n,...r}}))}};function Ot(e){let t=new I;return t.get(`/browsers`,async t=>{try{let r=e.get(n.BrowserService),i=e.get(n.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 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(n.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 r=t.req.param(`id`),i=e.get(n.BrowserService);return i.getBrowser(r)?(await i.closeBrowser(r),t.json({success:!0,message:`Browser "${r}" closed`})):t.json({error:`Browser "${r}" 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 r=t.req.param(`id`),i=e.get(n.PageRegistry).get(r);return i?i.page?(await i.page.close(),t.json({success:!0,message:`Page "${r}" closed`})):t.json({error:`Page "${r}" is in extension mode and cannot be closed via API`},400):t.json({error:`Page "${r}" 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 kt(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 jt(e){return At(new Date().getTime()-e.getTime())}const Mt=`table-container`,Nt=`stat-card`,Pt=`browser-row`,Ft=`page-row`,X=`status-active`,It=`url-cell`,Z=`timestamp-cell`,Lt=`actions-cell`,Rt=`kill-btn`,zt=`empty-state`;function Bt({browsers:e}){return e.length===0?L(`div`,{class:Mt,children:R(`div`,{class:zt,children:[L(`h3`,{children:`No Active Browsers`}),L(`p`,{children:`Launch a browser using MCP tools to see it here.`})]})}):L(`div`,{class:Mt,children:R(`table`,{class:`browser-table`,children:[L(`thead`,{children:R(`tr`,{children:[L(`th`,{children:`ID`}),L(`th`,{children:`Status`}),L(`th`,{children:`URL / Title`}),L(`th`,{children:`Created`}),L(`th`,{children:`Age`}),L(`th`,{children:`Actions`})]})}),L(`tbody`,{id:`browser-table-body`,children:e.map(e=>R(me,{children:[R(`tr`,{class:Pt,children:[L(`td`,{children:e.id}),L(`td`,{children:R(`span`,{class:X,children:[e.pages.length,` page`,e.pages.length===1?``:`s`]})}),L(`td`,{children:e.profileName||`Default Profile`}),L(`td`,{class:Z,children:kt(e.createdAt)}),L(`td`,{class:Z,children:jt(e.createdAt)}),L(`td`,{class:Lt,children:L(`button`,{type:`button`,class:Rt,onclick:`dashboard.killBrowser('${e.id}')`,children:`Kill`})})]}),e.pages.map(t=>R(`tr`,{class:Ft,children:[R(`td`,{children:[t.id,e.currentPageId===t.id&&` (active)`]}),L(`td`,{children:L(`span`,{class:X,children:`Open`})}),L(`td`,{class:It,title:t.url,children:t.title||t.url||`about:blank`}),L(`td`,{class:Z,children:kt(t.createdAt)}),L(`td`,{class:Z,children:jt(t.createdAt)}),L(`td`,{class:Lt,children:L(`button`,{type:`button`,class:Rt,onclick:`dashboard.killPage('${t.id}')`,children:`Close`})})]},t.id))]},e.id))})]})})}function Vt({stats:e}){return R(`div`,{class:`stats-container`,children:[R(`div`,{class:Nt,children:[L(`h3`,{children:`Active Browsers`}),L(`div`,{class:`value`,children:e.totalBrowsers})]}),R(`div`,{class:Nt,children:[L(`h3`,{children:`Active Pages`}),L(`div`,{class:`value`,children:e.totalPages})]})]})}function Ht({browsers:e,stats:t}){return R(`div`,{class:`dashboard-container`,children:[R(`div`,{class:`dashboard-header`,children:[R(`div`,{children:[L(`h1`,{children:`Playwright MCP Dashboard`}),L(`p`,{children:`Browser automation management (auto-refresh every 3 seconds)`})]}),R(`div`,{children:[L(`button`,{type:`button`,id:`refresh-btn`,class:`btn refresh-btn`,onclick:`dashboard.manualRefresh()`,children:`Refresh Now`}),L(`button`,{type:`button`,id:`kill-all-btn`,class:`btn btn-danger`,onclick:`dashboard.killAllBrowsers()`,children:`Kill All Browsers`})]})]}),L(Vt,{stats:t}),L(Bt,{browsers:e}),L(`script`,{children:pe(`
|
|
@@ -5483,11 +5483,31 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5483
5483
|
if (!normalizedActual || !normalizedExpected) return false;
|
|
5484
5484
|
return exact ? normalizedActual === normalizedExpected : normalizedActual.toLowerCase().includes(normalizedExpected.toLowerCase());
|
|
5485
5485
|
};
|
|
5486
|
+
const collectSearchRoots = (root, roots = []) => {
|
|
5487
|
+
roots.push(root);
|
|
5488
|
+
root.querySelectorAll("*").forEach((element) => {
|
|
5489
|
+
if (element.shadowRoot) collectSearchRoots(element.shadowRoot, roots);
|
|
5490
|
+
});
|
|
5491
|
+
return roots;
|
|
5492
|
+
};
|
|
5493
|
+
const getSearchRoots = () => collectSearchRoots(document);
|
|
5494
|
+
const querySelectorDeep = (selector, roots = getSearchRoots()) => {
|
|
5495
|
+
for (const root of roots) {
|
|
5496
|
+
const element = root.querySelector(selector);
|
|
5497
|
+
if (element) return element;
|
|
5498
|
+
}
|
|
5499
|
+
return null;
|
|
5500
|
+
};
|
|
5501
|
+
const querySelectorAllDeep = (selector, roots = getSearchRoots()) => roots.flatMap((root) => Array.from(root.querySelectorAll(selector)));
|
|
5502
|
+
const queryById = (id, rootNode = document) => {
|
|
5503
|
+
const escapedId = CSS.escape(id);
|
|
5504
|
+
return (rootNode instanceof Document || rootNode instanceof ShadowRoot ? rootNode : document).querySelector(`#${escapedId}`) ?? querySelectorDeep(`#${escapedId}`);
|
|
5505
|
+
};
|
|
5486
5506
|
const getAssociatedControl = (label) => {
|
|
5487
5507
|
if (label.control) return label.control;
|
|
5488
5508
|
const forAttr = label.getAttribute("for");
|
|
5489
5509
|
if (forAttr) {
|
|
5490
|
-
const target =
|
|
5510
|
+
const target = queryById(forAttr, label.getRootNode());
|
|
5491
5511
|
if (target) return target;
|
|
5492
5512
|
}
|
|
5493
5513
|
return label.querySelector("input,select,textarea,button");
|
|
@@ -5546,12 +5566,12 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5546
5566
|
if (ariaLabel) return normalizeText(ariaLabel);
|
|
5547
5567
|
const labelledBy = element.getAttribute("aria-labelledby");
|
|
5548
5568
|
if (labelledBy) {
|
|
5549
|
-
const labels = labelledBy.split(/\s+/).map((id) => normalizeText(
|
|
5569
|
+
const labels = labelledBy.split(/\s+/).map((id) => normalizeText(queryById(id, element.getRootNode())?.textContent)).filter(Boolean);
|
|
5550
5570
|
if (labels.length > 0) return labels.join(" ");
|
|
5551
5571
|
}
|
|
5552
5572
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
5553
5573
|
if (element.id) {
|
|
5554
|
-
const label =
|
|
5574
|
+
const label = querySelectorDeep(`label[for="${CSS.escape(element.id)}"]`);
|
|
5555
5575
|
if (label?.textContent) return normalizeText(label.textContent);
|
|
5556
5576
|
}
|
|
5557
5577
|
if (element.placeholder) return normalizeText(element.placeholder);
|
|
@@ -5561,26 +5581,25 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5561
5581
|
return normalizeText(element.textContent);
|
|
5562
5582
|
};
|
|
5563
5583
|
const findByText = (text, exact = false) => {
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
if (match instanceof HTMLLabelElement) {
|
|
5569
|
-
const control = getAssociatedControl(match);
|
|
5584
|
+
for (const element of querySelectorAllDeep("*")) {
|
|
5585
|
+
if (!matchesText(element.textContent, text, exact)) continue;
|
|
5586
|
+
if (element instanceof HTMLLabelElement) {
|
|
5587
|
+
const control = getAssociatedControl(element);
|
|
5570
5588
|
if (control) return control;
|
|
5571
5589
|
}
|
|
5572
|
-
return
|
|
5590
|
+
return element;
|
|
5573
5591
|
}
|
|
5574
5592
|
return null;
|
|
5575
5593
|
};
|
|
5576
5594
|
const locateElement = () => {
|
|
5595
|
+
const roots = getSearchRoots();
|
|
5577
5596
|
if (typeof locator.role === "string" && locator.role) {
|
|
5578
|
-
const elements =
|
|
5597
|
+
const elements = querySelectorAllDeep("*", roots).filter((element) => getRole(element) === locator.role);
|
|
5579
5598
|
const candidate = typeof locator.name === "string" && locator.name ? elements.find((element) => matchesText(getAccessibleName(element), locator.name, Boolean(locator.exact))) : elements[0];
|
|
5580
5599
|
if (candidate) return candidate;
|
|
5581
5600
|
}
|
|
5582
5601
|
if (typeof locator.label === "string" && locator.label) {
|
|
5583
|
-
const labels =
|
|
5602
|
+
const labels = querySelectorAllDeep("label", roots).filter((element) => matchesText(element.textContent, locator.label, Boolean(locator.exact)));
|
|
5584
5603
|
for (const label of labels) {
|
|
5585
5604
|
const control = getAssociatedControl(label);
|
|
5586
5605
|
if (control) return control;
|
|
@@ -5588,7 +5607,7 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5588
5607
|
if (labels[0]) return labels[0];
|
|
5589
5608
|
}
|
|
5590
5609
|
if (typeof locator.placeholder === "string" && locator.placeholder) {
|
|
5591
|
-
const element =
|
|
5610
|
+
const element = querySelectorAllDeep("[placeholder]", roots).find((candidate) => matchesText(candidate.getAttribute("placeholder"), locator.placeholder, Boolean(locator.exact)));
|
|
5592
5611
|
if (element) return element;
|
|
5593
5612
|
}
|
|
5594
5613
|
if (typeof locator.xpath === "string" && locator.xpath) {
|
|
@@ -5596,11 +5615,11 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5596
5615
|
if (result.singleNodeValue instanceof Element) return result.singleNodeValue;
|
|
5597
5616
|
}
|
|
5598
5617
|
if (typeof locator.testId === "string" && locator.testId) {
|
|
5599
|
-
const element =
|
|
5618
|
+
const element = querySelectorDeep(`[data-testid="${CSS.escape(locator.testId)}"]`, roots);
|
|
5600
5619
|
if (element) return element;
|
|
5601
5620
|
}
|
|
5602
5621
|
if (typeof locator.selector === "string" && locator.selector) {
|
|
5603
|
-
const element =
|
|
5622
|
+
const element = querySelectorDeep(locator.selector, roots);
|
|
5604
5623
|
if (element) return element;
|
|
5605
5624
|
}
|
|
5606
5625
|
if (typeof locator.text === "string" && locator.text) {
|
|
@@ -5608,19 +5627,19 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5608
5627
|
if (element) return element;
|
|
5609
5628
|
}
|
|
5610
5629
|
if (typeof locator.uid === "string" && locator.uid) {
|
|
5611
|
-
const element =
|
|
5630
|
+
const element = querySelectorDeep(`[data-browse-tool-uid="${locator.uid}"]`, roots);
|
|
5612
5631
|
if (element) return element;
|
|
5613
5632
|
}
|
|
5614
|
-
return
|
|
5633
|
+
return querySelectorDeep("input[type=\"file\"]", roots);
|
|
5615
5634
|
};
|
|
5616
5635
|
const resolveFileInput = (element) => {
|
|
5617
5636
|
if (!element) return null;
|
|
5618
5637
|
if (element instanceof HTMLInputElement && element.type === "file") return element;
|
|
5619
5638
|
if (element instanceof HTMLLabelElement && element.htmlFor) {
|
|
5620
|
-
const labelled =
|
|
5639
|
+
const labelled = queryById(element.htmlFor, element.getRootNode());
|
|
5621
5640
|
if (labelled instanceof HTMLInputElement && labelled.type === "file") return labelled;
|
|
5622
5641
|
}
|
|
5623
|
-
const nested = element.querySelector("input[type=\"file\"]");
|
|
5642
|
+
const nested = element.querySelector("input[type=\"file\"]") ?? (element.shadowRoot ? querySelectorDeep("input[type=\"file\"]", [element.shadowRoot]) : null);
|
|
5624
5643
|
return nested instanceof HTMLInputElement ? nested : null;
|
|
5625
5644
|
};
|
|
5626
5645
|
const input = resolveFileInput(locateElement());
|
|
@@ -5641,15 +5660,27 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5641
5660
|
};
|
|
5642
5661
|
await globalThis.chrome.debugger.attach({ tabId }, "1.3");
|
|
5643
5662
|
try {
|
|
5644
|
-
const
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5663
|
+
const objectResult = await globalThis.chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
|
|
5664
|
+
expression: `(() => {
|
|
5665
|
+
const collectRoots = (root, roots = []) => {
|
|
5666
|
+
roots.push(root);
|
|
5667
|
+
root.querySelectorAll('*').forEach((element) => {
|
|
5668
|
+
if (element.shadowRoot) collectRoots(element.shadowRoot, roots);
|
|
5669
|
+
});
|
|
5670
|
+
return roots;
|
|
5671
|
+
};
|
|
5672
|
+
for (const root of collectRoots(document)) {
|
|
5673
|
+
const input = root.querySelector('input[${markerAttr}="${markerValue}"]');
|
|
5674
|
+
if (input) return input;
|
|
5675
|
+
}
|
|
5676
|
+
return null;
|
|
5677
|
+
})()`,
|
|
5678
|
+
returnByValue: false
|
|
5649
5679
|
});
|
|
5650
|
-
|
|
5680
|
+
const objectId = objectResult.result?.objectId;
|
|
5681
|
+
if (!objectId || objectResult.result?.subtype === "null") throw new Error("Failed to resolve file input node");
|
|
5651
5682
|
await globalThis.chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", {
|
|
5652
|
-
|
|
5683
|
+
objectId,
|
|
5653
5684
|
files
|
|
5654
5685
|
});
|
|
5655
5686
|
} finally {
|
|
@@ -5662,8 +5693,20 @@ async function uploadFileHandler(args, _context, tabId) {
|
|
|
5662
5693
|
world: "ISOLATED",
|
|
5663
5694
|
args: [markerAttr, markerValue],
|
|
5664
5695
|
func: (attrName, attrValue) => {
|
|
5665
|
-
const
|
|
5666
|
-
|
|
5696
|
+
const collectRoots = (root, roots = []) => {
|
|
5697
|
+
roots.push(root);
|
|
5698
|
+
root.querySelectorAll("*").forEach((element) => {
|
|
5699
|
+
if (element.shadowRoot) collectRoots(element.shadowRoot, roots);
|
|
5700
|
+
});
|
|
5701
|
+
return roots;
|
|
5702
|
+
};
|
|
5703
|
+
for (const root of collectRoots(document)) {
|
|
5704
|
+
const element = root.querySelector(`input[${attrName}="${attrValue}"]`);
|
|
5705
|
+
if (element instanceof HTMLInputElement) {
|
|
5706
|
+
element.removeAttribute(attrName);
|
|
5707
|
+
return;
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5667
5710
|
}
|
|
5668
5711
|
});
|
|
5669
5712
|
} catch {}
|