@midscene/web 1.9.7 → 1.9.8-beta-20260618014851.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/es/agent-init-args.mjs +19 -0
  2. package/dist/es/agent-init-args.mjs.map +1 -0
  3. package/dist/es/{mcp-tools-cdp.mjs → agent-tools-cdp.mjs} +28 -12
  4. package/dist/es/agent-tools-cdp.mjs.map +1 -0
  5. package/dist/es/{mcp-tools-puppeteer.mjs → agent-tools-puppeteer.mjs} +31 -14
  6. package/dist/es/agent-tools-puppeteer.mjs.map +1 -0
  7. package/dist/es/{mcp-tools.mjs → agent-tools.mjs} +49 -13
  8. package/dist/es/agent-tools.mjs.map +1 -0
  9. package/dist/es/bridge-mode/io-client.mjs +1 -1
  10. package/dist/es/bridge-mode/io-server.mjs +2 -2
  11. package/dist/es/bridge-mode/io-server.mjs.map +1 -1
  12. package/dist/es/bridge-mode/page-browser-side.mjs +1 -1
  13. package/dist/es/bridge-mode/page-browser-side.mjs.map +1 -1
  14. package/dist/es/cdp-proxy-constants.mjs.map +1 -1
  15. package/dist/es/cdp-proxy-manager.mjs +1 -1
  16. package/dist/es/cdp-proxy-manager.mjs.map +1 -1
  17. package/dist/es/cdp-proxy.mjs.map +1 -1
  18. package/dist/es/cdp-target-store.mjs +1 -1
  19. package/dist/es/cdp-target-store.mjs.map +1 -1
  20. package/dist/es/cli.mjs +4 -4
  21. package/dist/es/cli.mjs.map +1 -1
  22. package/dist/es/index.mjs +2 -2
  23. package/dist/es/puppeteer/agent-launcher.mjs +11 -1
  24. package/dist/es/puppeteer/agent-launcher.mjs.map +1 -1
  25. package/dist/lib/agent-init-args.js +56 -0
  26. package/dist/lib/agent-init-args.js.map +1 -0
  27. package/dist/lib/{mcp-tools-cdp.js → agent-tools-cdp.js} +27 -11
  28. package/dist/lib/agent-tools-cdp.js.map +1 -0
  29. package/dist/lib/{mcp-tools-puppeteer.js → agent-tools-puppeteer.js} +30 -13
  30. package/dist/lib/agent-tools-puppeteer.js.map +1 -0
  31. package/dist/lib/{mcp-tools.js → agent-tools.js} +48 -12
  32. package/dist/lib/agent-tools.js.map +1 -0
  33. package/dist/lib/bridge-mode/io-client.js +1 -1
  34. package/dist/lib/bridge-mode/io-server.js +2 -2
  35. package/dist/lib/bridge-mode/io-server.js.map +1 -1
  36. package/dist/lib/bridge-mode/page-browser-side.js +1 -1
  37. package/dist/lib/bridge-mode/page-browser-side.js.map +1 -1
  38. package/dist/lib/cdp-proxy-constants.js.map +1 -1
  39. package/dist/lib/cdp-proxy-manager.js +1 -1
  40. package/dist/lib/cdp-proxy-manager.js.map +1 -1
  41. package/dist/lib/cdp-proxy.js.map +1 -1
  42. package/dist/lib/cdp-target-store.js +1 -1
  43. package/dist/lib/cdp-target-store.js.map +1 -1
  44. package/dist/lib/cli.js +5 -5
  45. package/dist/lib/cli.js.map +1 -1
  46. package/dist/lib/index.js +4 -4
  47. package/dist/lib/puppeteer/agent-launcher.js +14 -0
  48. package/dist/lib/puppeteer/agent-launcher.js.map +1 -1
  49. package/dist/types/agent-init-args.d.ts +13 -0
  50. package/dist/types/{mcp-tools-cdp.d.ts → agent-tools-cdp.d.ts} +8 -5
  51. package/dist/types/{mcp-tools-puppeteer.d.ts → agent-tools-puppeteer.d.ts} +8 -5
  52. package/dist/types/agent-tools.d.ts +17 -0
  53. package/dist/types/cdp-proxy-manager.d.ts +1 -1
  54. package/dist/types/cdp-proxy.d.ts +1 -1
  55. package/dist/types/index.d.ts +2 -2
  56. package/dist/types/puppeteer/agent-launcher.d.ts +2 -1
  57. package/package.json +4 -9
  58. package/dist/es/mcp-server.mjs +0 -35
  59. package/dist/es/mcp-server.mjs.map +0 -1
  60. package/dist/es/mcp-tools-cdp.mjs.map +0 -1
  61. package/dist/es/mcp-tools-puppeteer.mjs.map +0 -1
  62. package/dist/es/mcp-tools.mjs.map +0 -1
  63. package/dist/lib/mcp-server.js +0 -75
  64. package/dist/lib/mcp-server.js.map +0 -1
  65. package/dist/lib/mcp-tools-cdp.js.map +0 -1
  66. package/dist/lib/mcp-tools-puppeteer.js.map +0 -1
  67. package/dist/lib/mcp-tools.js.map +0 -1
  68. package/dist/types/mcp-server.d.ts +0 -26
  69. package/dist/types/mcp-tools.d.ts +0 -14
@@ -1 +1 @@
1
- {"version":3,"file":"bridge-mode/page-browser-side.mjs","sources":["../../../src/bridge-mode/page-browser-side.ts"],"sourcesContent":["import { assert } from '@midscene/shared/utils';\nimport ChromeExtensionProxyPage from '../chrome-extension/page';\nimport type {\n ChromePageDestroyOptions,\n KeyboardAction,\n MouseAction,\n} from '../web-page';\nimport {\n type BridgeConnectTabOptions,\n BridgeEvent,\n DefaultBridgeServerPort,\n KeyboardEvent,\n MouseEvent,\n} from './common';\nimport { BridgeClient } from './io-client';\n\ndeclare const __VERSION__: string;\n\nconst NEW_TAB_LOAD_TIMEOUT_MS = 30_000;\n\nfunction isBlankUrl(url: string | undefined): boolean {\n if (!url) return true;\n return url === 'about:blank' || url.startsWith('chrome://newtab');\n}\n\n// Wait until the freshly created tab has navigated away from about:blank\n// and reached `status === 'complete'`. Resolves on timeout instead of\n// throwing so callers degrade to the existing lazy-attach behavior.\nfunction waitForTabNavigationComplete(\n tabId: number,\n targetUrl: string,\n timeoutMs = NEW_TAB_LOAD_TIMEOUT_MS,\n): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = () => {\n if (settled) return;\n settled = true;\n try {\n chrome.tabs.onUpdated.removeListener(onUpdated);\n } catch {}\n clearTimeout(timer);\n resolve();\n };\n\n const isReady = (tab: chrome.tabs.Tab | undefined): boolean => {\n if (!tab) return false;\n if (tab.status !== 'complete') return false;\n const currentUrl = tab.url || tab.pendingUrl || '';\n // Skip the initial about:blank \"complete\" that fires before\n // the target URL navigation kicks in.\n if (isBlankUrl(currentUrl) && !isBlankUrl(targetUrl)) return false;\n return true;\n };\n\n const onUpdated = (\n id: number,\n _info: chrome.tabs.TabChangeInfo,\n tab: chrome.tabs.Tab,\n ) => {\n if (id !== tabId) return;\n if (isReady(tab)) finish();\n };\n\n chrome.tabs.onUpdated.addListener(onUpdated);\n const timer = setTimeout(finish, timeoutMs);\n\n // Handle the race where the tab already finished loading before\n // we registered the listener.\n chrome.tabs\n .get(tabId)\n .then((tab) => {\n if (isReady(tab)) finish();\n })\n .catch(() => {});\n });\n}\n\nexport class ExtensionBridgePageBrowserSide extends ChromeExtensionProxyPage {\n public bridgeClient: BridgeClient | null = null;\n\n private destroyOptions?: ChromePageDestroyOptions;\n\n private newlyCreatedTabIds: number[] = [];\n\n // Connection confirmation state\n private confirmationPromise: Promise<boolean> | null = null;\n\n constructor(\n public serverEndpoint?: string,\n public onDisconnect: () => void = () => {},\n public onLogMessage: (\n message: string,\n type: 'log' | 'status',\n ) => void = () => {},\n forceSameTabNavigation = true,\n public onConnectionRequest?: () => Promise<boolean>,\n ) {\n super(forceSameTabNavigation);\n }\n\n private async setupBridgeClient() {\n const endpoint =\n this.serverEndpoint || `ws://localhost:${DefaultBridgeServerPort}`;\n\n // Create confirmation gate BEFORE establishing connection,\n // so that any calls received immediately after connection are blocked\n // until user confirms. This prevents a race condition where server-side\n // queued calls bypass the confirmation dialog.\n let resolveConfirmationGate: (allowed: boolean) => void = () => {};\n if (this.onConnectionRequest) {\n this.confirmationPromise = new Promise<boolean>((resolve) => {\n resolveConfirmationGate = resolve;\n });\n }\n\n this.bridgeClient = new BridgeClient(\n endpoint,\n async (method, args: any[]) => {\n // Wait for user confirmation before processing any commands\n if (this.confirmationPromise) {\n const allowed = await this.confirmationPromise;\n if (!allowed) {\n throw new Error('Connection denied by user');\n }\n }\n\n this.onLogMessage(`bridge call from cli side: ${method}`, 'log');\n if (method === BridgeEvent.ConnectNewTabWithUrl) {\n return this.connectNewTabWithUrl.apply(\n this,\n args as unknown as [string],\n );\n }\n\n if (method === BridgeEvent.GetBrowserTabList) {\n return this.getBrowserTabList.apply(this, args as any);\n }\n\n if (method === BridgeEvent.SetActiveTabId) {\n return this.setActiveTabId.apply(this, args as any);\n }\n\n if (method === BridgeEvent.ConnectCurrentTab) {\n return this.connectCurrentTab.apply(this, args as any);\n }\n\n if (method === BridgeEvent.UpdateAgentStatus) {\n return this.onLogMessage(args[0] as string, 'status');\n }\n\n const tabId = await this.getActiveTabId();\n if (!tabId || tabId === 0) {\n throw new Error('no tab is connected');\n }\n\n // this.onLogMessage(`calling method: ${method}`);\n\n if (method.startsWith(MouseEvent.PREFIX)) {\n const actionName = method.split('.')[1] as keyof MouseAction;\n if (actionName === 'drag') {\n return this.mouse[actionName].apply(this.mouse, args as any);\n }\n return this.mouse[actionName].apply(this.mouse, args as any);\n }\n\n if (method.startsWith(KeyboardEvent.PREFIX)) {\n const actionName = method.split('.')[1] as keyof KeyboardAction;\n if (actionName === 'press') {\n return this.keyboard[actionName].apply(this.keyboard, args as any);\n }\n return this.keyboard[actionName].apply(this.keyboard, args as any);\n }\n\n if (!this[method as keyof ChromeExtensionProxyPage]) {\n this.onLogMessage(`method not found: ${method}`, 'log');\n return undefined;\n }\n\n try {\n // @ts-expect-error\n const result = await this[method as keyof ChromeExtensionProxyPage](\n ...args,\n );\n return result;\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Unknown error';\n this.onLogMessage(\n `Error calling method: ${method}, ${errorMessage}`,\n 'log',\n );\n throw new Error(errorMessage, { cause: e });\n }\n },\n // on disconnect\n () => {\n return this.destroy();\n },\n );\n await this.bridgeClient.connect();\n\n // Show confirmation dialog after connection is established\n if (this.onConnectionRequest) {\n this.onLogMessage('Waiting for user confirmation...', 'log');\n const allowed = await this.onConnectionRequest();\n resolveConfirmationGate(allowed);\n this.confirmationPromise = null;\n\n if (!allowed) {\n this.onLogMessage('Connection denied by user', 'log');\n this.bridgeClient.disconnect();\n this.bridgeClient = null;\n throw new Error('Connection denied by user');\n }\n }\n\n this.onLogMessage(\n `Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v${__VERSION__}`,\n 'log',\n );\n }\n\n public async connect() {\n return await this.setupBridgeClient();\n }\n\n public async connectNewTabWithUrl(\n url: string,\n options: BridgeConnectTabOptions = {\n forceSameTabNavigation: true,\n },\n ) {\n const tab = await chrome.tabs.create({ url });\n const tabId = tab.id;\n assert(tabId, 'failed to get tabId after creating a new tab');\n\n // new tab\n this.onLogMessage(`Creating new tab: ${url}`, 'log');\n this.newlyCreatedTabIds.push(tabId);\n\n if (options?.forceSameTabNavigation) {\n this.forceSameTabNavigation = true;\n }\n\n // chrome.tabs.create returns immediately with an about:blank target,\n // then navigates to `url`. If we attach the debugger during that\n // cross-origin transition Site Isolation will detach it again, leaving\n // the first CDP command to fail with \"Debugger is not attached to the\n // tab\". Wait for navigation to settle so the lazy attach lands on a\n // stable target.\n await waitForTabNavigationComplete(tabId, url);\n\n await this.setActiveTabId(tabId);\n }\n\n public async connectCurrentTab(\n options: BridgeConnectTabOptions = {\n forceSameTabNavigation: true,\n },\n ) {\n const tabs = await chrome.tabs.query({ active: true, currentWindow: true });\n const tabId = tabs[0]?.id;\n assert(tabId, 'failed to get tabId');\n\n this.onLogMessage(`Connected to current tab: ${tabs[0]?.url}`, 'log');\n\n if (options?.forceSameTabNavigation) {\n this.forceSameTabNavigation = true;\n }\n\n await this.setActiveTabId(tabId);\n }\n\n public async setDestroyOptions(options: ChromePageDestroyOptions) {\n this.destroyOptions = options;\n }\n\n async destroy() {\n if (this.destroyOptions?.closeTab && this.newlyCreatedTabIds.length > 0) {\n this.onLogMessage('Closing all newly created tabs by bridge...', 'log');\n for (const tabId of this.newlyCreatedTabIds) {\n await chrome.tabs.remove(tabId);\n }\n this.newlyCreatedTabIds = [];\n }\n\n await super.destroy();\n\n if (this.bridgeClient) {\n this.bridgeClient.disconnect();\n this.bridgeClient = null;\n this.onDisconnect();\n }\n }\n}\n"],"names":["NEW_TAB_LOAD_TIMEOUT_MS","isBlankUrl","url","waitForTabNavigationComplete","tabId","targetUrl","timeoutMs","Promise","resolve","settled","finish","chrome","onUpdated","clearTimeout","timer","isReady","tab","currentUrl","id","_info","setTimeout","ExtensionBridgePageBrowserSide","ChromeExtensionProxyPage","endpoint","DefaultBridgeServerPort","resolveConfirmationGate","BridgeClient","method","args","allowed","Error","BridgeEvent","MouseEvent","actionName","KeyboardEvent","result","e","errorMessage","options","assert","tabs","serverEndpoint","onDisconnect","onLogMessage","forceSameTabNavigation","onConnectionRequest"],"mappings":"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"}
1
+ {"version":3,"file":"bridge-mode/page-browser-side.mjs","sources":["../../../src/bridge-mode/page-browser-side.ts"],"sourcesContent":["import { assert } from '@midscene/shared/utils';\nimport ChromeExtensionProxyPage from '../chrome-extension/page';\nimport type {\n ChromePageDestroyOptions,\n KeyboardAction,\n MouseAction,\n} from '../web-page';\nimport {\n type BridgeConnectTabOptions,\n BridgeEvent,\n DefaultBridgeServerPort,\n KeyboardEvent,\n MouseEvent,\n} from './common';\nimport { BridgeClient } from './io-client';\n\ndeclare const __VERSION__: string;\n\nconst NEW_TAB_LOAD_TIMEOUT_MS = 30_000;\n\nfunction isBlankUrl(url: string | undefined): boolean {\n if (!url) return true;\n return url === 'about:blank' || url.startsWith('chrome://newtab');\n}\n\n// Wait until the freshly created tab has navigated away from about:blank\n// and reached `status === 'complete'`. Resolves on timeout instead of\n// throwing so callers degrade to the existing lazy-attach behavior.\nfunction waitForTabNavigationComplete(\n tabId: number,\n targetUrl: string,\n timeoutMs = NEW_TAB_LOAD_TIMEOUT_MS,\n): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = () => {\n if (settled) return;\n settled = true;\n try {\n chrome.tabs.onUpdated.removeListener(onUpdated);\n } catch {}\n clearTimeout(timer);\n resolve();\n };\n\n const isReady = (tab: chrome.tabs.Tab | undefined): boolean => {\n if (!tab) return false;\n if (tab.status !== 'complete') return false;\n const currentUrl = tab.url || tab.pendingUrl || '';\n // Skip the initial about:blank \"complete\" that fires before\n // the target URL navigation kicks in.\n if (isBlankUrl(currentUrl) && !isBlankUrl(targetUrl)) return false;\n return true;\n };\n\n const onUpdated = (\n id: number,\n _info: chrome.tabs.TabChangeInfo,\n tab: chrome.tabs.Tab,\n ) => {\n if (id !== tabId) return;\n if (isReady(tab)) finish();\n };\n\n chrome.tabs.onUpdated.addListener(onUpdated);\n const timer = setTimeout(finish, timeoutMs);\n\n // Handle the race where the tab already finished loading before\n // we registered the listener.\n chrome.tabs\n .get(tabId)\n .then((tab) => {\n if (isReady(tab)) finish();\n })\n .catch(() => {});\n });\n}\n\nexport class ExtensionBridgePageBrowserSide extends ChromeExtensionProxyPage {\n public bridgeClient: BridgeClient | null = null;\n\n private destroyOptions?: ChromePageDestroyOptions;\n\n private newlyCreatedTabIds: number[] = [];\n\n // Connection confirmation state\n private confirmationPromise: Promise<boolean> | null = null;\n\n constructor(\n public serverEndpoint?: string,\n public onDisconnect: () => void = () => {},\n public onLogMessage: (\n message: string,\n type: 'log' | 'status',\n ) => void = () => {},\n forceSameTabNavigation = true,\n public onConnectionRequest?: () => Promise<boolean>,\n ) {\n super(forceSameTabNavigation);\n }\n\n private async setupBridgeClient() {\n const endpoint =\n this.serverEndpoint || `ws://localhost:${DefaultBridgeServerPort}`;\n\n // Create confirmation gate BEFORE establishing connection,\n // so that any calls received immediately after connection are blocked\n // until user confirms. This prevents a race condition where server-side\n // queued calls bypass the confirmation dialog.\n let resolveConfirmationGate: (allowed: boolean) => void = () => {};\n if (this.onConnectionRequest) {\n this.confirmationPromise = new Promise<boolean>((resolve) => {\n resolveConfirmationGate = resolve;\n });\n }\n\n this.bridgeClient = new BridgeClient(\n endpoint,\n async (method, args: any[]) => {\n // Wait for user confirmation before processing any commands\n if (this.confirmationPromise) {\n const allowed = await this.confirmationPromise;\n if (!allowed) {\n throw new Error('Connection denied by user');\n }\n }\n\n this.onLogMessage(`bridge call from cli side: ${method}`, 'log');\n if (method === BridgeEvent.ConnectNewTabWithUrl) {\n return this.connectNewTabWithUrl.apply(\n this,\n args as unknown as [string],\n );\n }\n\n if (method === BridgeEvent.GetBrowserTabList) {\n return this.getBrowserTabList.apply(this, args as any);\n }\n\n if (method === BridgeEvent.SetActiveTabId) {\n return this.setActiveTabId.apply(this, args as any);\n }\n\n if (method === BridgeEvent.ConnectCurrentTab) {\n return this.connectCurrentTab.apply(this, args as any);\n }\n\n if (method === BridgeEvent.UpdateAgentStatus) {\n return this.onLogMessage(args[0] as string, 'status');\n }\n\n const tabId = await this.getActiveTabId();\n if (!tabId || tabId === 0) {\n throw new Error('no tab is connected');\n }\n\n // this.onLogMessage(`calling method: ${method}`);\n\n if (method.startsWith(MouseEvent.PREFIX)) {\n const actionName = method.split('.')[1] as keyof MouseAction;\n if (actionName === 'drag') {\n return this.mouse[actionName].apply(this.mouse, args as any);\n }\n return this.mouse[actionName].apply(this.mouse, args as any);\n }\n\n if (method.startsWith(KeyboardEvent.PREFIX)) {\n const actionName = method.split('.')[1] as keyof KeyboardAction;\n if (actionName === 'press') {\n return this.keyboard[actionName].apply(this.keyboard, args as any);\n }\n return this.keyboard[actionName].apply(this.keyboard, args as any);\n }\n\n if (!this[method as keyof ChromeExtensionProxyPage]) {\n this.onLogMessage(`method not found: ${method}`, 'log');\n return undefined;\n }\n\n try {\n // @ts-expect-error\n const result = await this[method as keyof ChromeExtensionProxyPage](\n ...args,\n );\n return result;\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Unknown error';\n this.onLogMessage(\n `Error calling method: ${method}, ${errorMessage}`,\n 'log',\n );\n throw new Error(errorMessage, { cause: e });\n }\n },\n // on disconnect\n () => {\n return this.destroy();\n },\n );\n await this.bridgeClient.connect();\n\n // Show confirmation dialog after connection is established\n if (this.onConnectionRequest) {\n this.onLogMessage('Waiting for user confirmation...', 'log');\n const allowed = await this.onConnectionRequest();\n resolveConfirmationGate(allowed);\n this.confirmationPromise = null;\n\n if (!allowed) {\n this.onLogMessage('Connection denied by user', 'log');\n this.bridgeClient.disconnect();\n this.bridgeClient = null;\n throw new Error('Connection denied by user');\n }\n }\n\n this.onLogMessage(\n `Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v${__VERSION__}`,\n 'log',\n );\n }\n\n public async connect() {\n return await this.setupBridgeClient();\n }\n\n public async connectNewTabWithUrl(\n url: string,\n options: BridgeConnectTabOptions = {\n forceSameTabNavigation: true,\n },\n ) {\n const tab = await chrome.tabs.create({ url });\n const tabId = tab.id;\n assert(tabId, 'failed to get tabId after creating a new tab');\n\n // new tab\n this.onLogMessage(`Creating new tab: ${url}`, 'log');\n this.newlyCreatedTabIds.push(tabId);\n\n if (options?.forceSameTabNavigation) {\n this.forceSameTabNavigation = true;\n }\n\n // chrome.tabs.create returns immediately with an about:blank target,\n // then navigates to `url`. If we attach the debugger during that\n // cross-origin transition Site Isolation will detach it again, leaving\n // the first CDP command to fail with \"Debugger is not attached to the\n // tab\". Wait for navigation to settle so the lazy attach lands on a\n // stable target.\n await waitForTabNavigationComplete(tabId, url);\n\n await this.setActiveTabId(tabId);\n }\n\n public async connectCurrentTab(\n options: BridgeConnectTabOptions = {\n forceSameTabNavigation: true,\n },\n ) {\n const tabs = await chrome.tabs.query({ active: true, currentWindow: true });\n const tabId = tabs[0]?.id;\n assert(tabId, 'failed to get tabId');\n\n this.onLogMessage(`Connected to current tab: ${tabs[0]?.url}`, 'log');\n\n if (options?.forceSameTabNavigation) {\n this.forceSameTabNavigation = true;\n }\n\n await this.setActiveTabId(tabId);\n }\n\n public async setDestroyOptions(options: ChromePageDestroyOptions) {\n this.destroyOptions = options;\n }\n\n async destroy() {\n if (this.destroyOptions?.closeTab && this.newlyCreatedTabIds.length > 0) {\n this.onLogMessage('Closing all newly created tabs by bridge...', 'log');\n for (const tabId of this.newlyCreatedTabIds) {\n await chrome.tabs.remove(tabId);\n }\n this.newlyCreatedTabIds = [];\n }\n\n await super.destroy();\n\n if (this.bridgeClient) {\n this.bridgeClient.disconnect();\n this.bridgeClient = null;\n this.onDisconnect();\n }\n }\n}\n"],"names":["NEW_TAB_LOAD_TIMEOUT_MS","isBlankUrl","url","waitForTabNavigationComplete","tabId","targetUrl","timeoutMs","Promise","resolve","settled","finish","chrome","onUpdated","clearTimeout","timer","isReady","tab","currentUrl","id","_info","setTimeout","ExtensionBridgePageBrowserSide","ChromeExtensionProxyPage","endpoint","DefaultBridgeServerPort","resolveConfirmationGate","BridgeClient","method","args","allowed","Error","BridgeEvent","MouseEvent","actionName","KeyboardEvent","result","e","errorMessage","options","assert","tabs","serverEndpoint","onDisconnect","onLogMessage","forceSameTabNavigation","onConnectionRequest"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-proxy-constants.mjs","sources":["../../src/cdp-proxy-constants.ts"],"sourcesContent":["/**\n * Shared constants for CDP proxy discovery between cdp-proxy.ts and mcp-tools-cdp.ts.\n */\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nexport const PROXY_ENDPOINT_FILE = join(\n tmpdir(),\n 'midscene-cdp-proxy-endpoint',\n);\nexport const PROXY_PID_FILE = join(tmpdir(), 'midscene-cdp-proxy-pid');\nexport const PROXY_UPSTREAM_FILE = join(\n tmpdir(),\n 'midscene-cdp-proxy-upstream',\n);\nexport const TARGET_ID_FILE = join(tmpdir(), 'midscene-cdp-target-id');\n"],"names":["PROXY_ENDPOINT_FILE","join","tmpdir","PROXY_PID_FILE","PROXY_UPSTREAM_FILE","TARGET_ID_FILE"],"mappings":";;AAMO,MAAMA,sBAAsBC,KACjCC,UACA;AAEK,MAAMC,iBAAiBF,KAAKC,UAAU;AACtC,MAAME,sBAAsBH,KACjCC,UACA;AAEK,MAAMG,iBAAiBJ,KAAKC,UAAU"}
1
+ {"version":3,"file":"cdp-proxy-constants.mjs","sources":["../../src/cdp-proxy-constants.ts"],"sourcesContent":["/**\n * Shared constants for CDP proxy discovery between cdp-proxy.ts and agent-tools-cdp.ts.\n */\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nexport const PROXY_ENDPOINT_FILE = join(\n tmpdir(),\n 'midscene-cdp-proxy-endpoint',\n);\nexport const PROXY_PID_FILE = join(tmpdir(), 'midscene-cdp-proxy-pid');\nexport const PROXY_UPSTREAM_FILE = join(\n tmpdir(),\n 'midscene-cdp-proxy-upstream',\n);\nexport const TARGET_ID_FILE = join(tmpdir(), 'midscene-cdp-target-id');\n"],"names":["PROXY_ENDPOINT_FILE","join","tmpdir","PROXY_PID_FILE","PROXY_UPSTREAM_FILE","TARGET_ID_FILE"],"mappings":";;AAMO,MAAMA,sBAAsBC,KACjCC,UACA;AAEK,MAAMC,iBAAiBF,KAAKC,UAAU;AACtC,MAAME,sBAAsBH,KACjCC,UACA;AAEK,MAAMG,iBAAiBJ,KAAKC,UAAU"}
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { getDebug } from "@midscene/shared/logger";
6
6
  import { PROXY_ENDPOINT_FILE, PROXY_PID_FILE, PROXY_UPSTREAM_FILE } from "./cdp-proxy-constants.mjs";
7
7
  import { cleanupTargetIdFile } from "./cdp-target-store.mjs";
8
- const debug = getDebug('mcp:cdp:proxy');
8
+ const debug = getDebug('agent-tools:cdp:proxy');
9
9
  const PROXY_TERM_GRACE_MS = 2000;
10
10
  const PROXY_TERM_POLL_MS = 50;
11
11
  const PROXY_STDERR_BUFFER_LIMIT = 8192;
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-proxy-manager.mjs","sources":["../../src/cdp-proxy-manager.ts"],"sourcesContent":["/**\n * CDP proxy lifecycle manager (parent-side).\n *\n * Owns everything the CLI needs to start, locate, replace and kill the\n * standalone `cdp-proxy.ts` child process — including its on-disk\n * metadata files (PROXY_ENDPOINT_FILE / PROXY_PID_FILE /\n * PROXY_UPSTREAM_FILE). Also handles auto-resolving page-level CDP URLs\n * to browser-level endpoints via `/json/version`, since puppeteer-core\n * cannot connect to page-level URLs directly.\n *\n * Other modules (notably `mcp-tools-cdp.ts`) use this through the\n * `getProxyEndpoint()` entry point and never touch the metadata files\n * themselves. The cross-command targetId is a separate concern owned by\n * `cdp-target-store.ts`.\n */\n\nimport { spawn } from 'node:child_process';\nimport { existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport http from 'node:http';\nimport { join } from 'node:path';\nimport { getDebug } from '@midscene/shared/logger';\nimport {\n PROXY_ENDPOINT_FILE,\n PROXY_PID_FILE,\n PROXY_UPSTREAM_FILE,\n} from './cdp-proxy-constants';\nimport { cleanupTargetIdFile } from './cdp-target-store';\n\nconst debug = getDebug('mcp:cdp:proxy');\n\n/** Time to wait for the proxy to exit on SIGTERM before resorting to SIGKILL. */\nconst PROXY_TERM_GRACE_MS = 2000;\n\n/** Polling interval while waiting for the proxy's PID file to disappear. */\nconst PROXY_TERM_POLL_MS = 50;\n\n/** Keep at most this many bytes of proxy stderr for diagnostics. */\nconst PROXY_STDERR_BUFFER_LIMIT = 8 * 1024;\n\n/** How long `spawnProxy()` waits for the child to print its endpoint. */\nconst PROXY_STARTUP_TIMEOUT_MS = 10_000;\n\n/**\n * Page-level CDP URLs (`/devtools/page/<id>`) cannot be passed to\n * `puppeteer.connect()` — puppeteer needs a browser-level endpoint.\n */\nfunction isPageLevelEndpoint(endpoint: string): boolean {\n return /\\/devtools\\/page\\//.test(endpoint);\n}\n\n/**\n * Resolve a page-level CDP URL to its browser-level WebSocket URL by\n * fetching `/json/version` from the same host:port.\n */\nfunction resolveBrowserEndpoint(pageEndpoint: string): Promise<string> {\n return new Promise((resolve, reject) => {\n let host: string;\n try {\n const url = new URL(pageEndpoint);\n host = url.host; // includes port (e.g. \"127.0.0.1:9222\")\n } catch {\n reject(new Error(`Invalid CDP endpoint URL: ${pageEndpoint}`));\n return;\n }\n\n const req = http.get(\n `http://${host}/json/version`,\n { timeout: 5000 },\n (res) => {\n if (res.statusCode && res.statusCode >= 400) {\n reject(new Error(`/json/version returned HTTP ${res.statusCode}`));\n res.resume();\n return;\n }\n let data = '';\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString();\n });\n res.on('end', () => {\n try {\n const info = JSON.parse(data);\n if (info.webSocketDebuggerUrl) {\n resolve(info.webSocketDebuggerUrl);\n } else {\n reject(\n new Error(\n 'webSocketDebuggerUrl not found in /json/version response',\n ),\n );\n }\n } catch {\n reject(\n new Error(`Failed to parse /json/version response: ${data}`),\n );\n }\n });\n },\n );\n req.on('error', (err) =>\n reject(new Error(`Failed to fetch /json/version: ${err.message}`)),\n );\n req.on('timeout', () => {\n req.destroy();\n reject(new Error('Timeout fetching /json/version'));\n });\n });\n}\n\n/**\n * True if the previously spawned proxy process is still running.\n */\nexport function isProxyAlive(): boolean {\n if (!existsSync(PROXY_PID_FILE)) return false;\n try {\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n process.kill(pid, 0); // signal 0 = existence check\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * The local WebSocket URL the running proxy is serving, or null.\n */\nexport function readProxyEndpoint(): string | null {\n if (!existsSync(PROXY_ENDPOINT_FILE)) return null;\n try {\n return readFileSync(PROXY_ENDPOINT_FILE, 'utf-8').trim();\n } catch {\n return null;\n }\n}\n\n/**\n * The Chrome endpoint the running proxy is connected to, or null.\n */\nexport function readProxyUpstream(): string | null {\n if (!existsSync(PROXY_UPSTREAM_FILE)) return null;\n try {\n return readFileSync(PROXY_UPSTREAM_FILE, 'utf-8').trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Best-effort sweep of proxy metadata files. Only used as a fallback\n * when the proxy fails to exit and run its own `cleanupIfOwned()`. In\n * the happy path the child owns these files and removes them itself.\n */\nfunction sweepProxyMetadataFiles(): void {\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_UPSTREAM_FILE)) unlinkSync(PROXY_UPSTREAM_FILE);\n } catch {}\n}\n\n/**\n * Stop the running proxy and discard the cross-command targetId.\n *\n * Sends SIGTERM and waits for the proxy's own SIGTERM handler to remove\n * its PROXY_*_FILE metadata via `cleanupIfOwned()`. When the PID file\n * disappears we know the next `spawnProxy()` can safely take over\n * without tripping the duplicate-proxy guard. Falls back to SIGKILL +\n * manual sweep if the proxy is unresponsive within `PROXY_TERM_GRACE_MS`.\n *\n * The targetId file is cleared regardless of the proxy's state because\n * it points into the outgoing Chrome's tab list.\n */\nexport async function killProxy(): Promise<void> {\n cleanupTargetIdFile();\n\n if (!existsSync(PROXY_PID_FILE)) return;\n\n let pid: number;\n try {\n pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (!Number.isFinite(pid)) {\n sweepProxyMetadataFiles();\n return;\n }\n } catch (err) {\n debug('killProxy: cannot read pid file: %s', err);\n sweepProxyMetadataFiles();\n return;\n }\n\n try {\n process.kill(pid, 'SIGTERM');\n debug('Sent SIGTERM to proxy pid %d', pid);\n } catch (err) {\n // ESRCH (already dead) is the common case; surface anything else\n // (e.g. EPERM) via debug so it does not vanish silently. Either way\n // sweep the orphan metadata so the next spawn has a clean slate.\n debug('killProxy: SIGTERM failed (pid %d): %s', pid, err);\n sweepProxyMetadataFiles();\n return;\n }\n\n const deadline = Date.now() + PROXY_TERM_GRACE_MS;\n while (Date.now() < deadline && existsSync(PROXY_PID_FILE)) {\n await new Promise((r) => setTimeout(r, PROXY_TERM_POLL_MS));\n }\n\n if (existsSync(PROXY_PID_FILE)) {\n debug(\n 'proxy pid %d did not clean up within %dms, forcing SIGKILL',\n pid,\n PROXY_TERM_GRACE_MS,\n );\n try {\n process.kill(pid, 'SIGKILL');\n } catch {}\n sweepProxyMetadataFiles();\n }\n}\n\n/**\n * Spawn the CDP proxy process and wait for it to print the endpoint.\n *\n * Captures the child's stderr so that when startup fails we can surface\n * the real reason (upstream closed / duplicate proxy / upstream error)\n * instead of the generic \"exited before ready\".\n */\nfunction spawnProxy(chromeEndpoint: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const proxyScript = join(__dirname, 'cdp-proxy.js');\n const proc = spawn(process.execPath, [proxyScript, chromeEndpoint], {\n detached: true,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n proc.unref();\n\n let output = '';\n let stderrBuf = '';\n let settled = false;\n\n const appendStderr = (chunk: Buffer) => {\n stderrBuf += chunk.toString();\n if (stderrBuf.length > PROXY_STDERR_BUFFER_LIMIT) {\n stderrBuf = stderrBuf.slice(-PROXY_STDERR_BUFFER_LIMIT);\n }\n };\n proc.stderr!.on('data', appendStderr);\n\n const formatStderr = () => {\n const trimmed = stderrBuf.trim();\n return trimmed ? ` (stderr: ${trimmed})` : '';\n };\n\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(\n new Error(\n `Proxy startup timeout (${PROXY_STARTUP_TIMEOUT_MS / 1000}s)${formatStderr()}`,\n ),\n );\n }\n }, PROXY_STARTUP_TIMEOUT_MS);\n\n const onData = (chunk: Buffer) => {\n output += chunk.toString();\n const lines = output.split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line);\n if (parsed.endpoint && !settled) {\n settled = true;\n clearTimeout(timer);\n proc.stdout!.removeListener('data', onData);\n proc.stderr!.removeListener('data', appendStderr);\n // Destroy the stdio pipes so they don't keep the parent\n // process event loop alive after we've read the endpoint.\n proc.stdout!.destroy();\n proc.stderr!.destroy();\n resolve(parsed.endpoint);\n return;\n }\n } catch {\n // stdout may contain non-JSON lines during startup — skip them\n }\n }\n };\n proc.stdout!.on('data', onData);\n\n proc.on('error', (err) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n reject(new Error(`Failed to spawn proxy: ${err.message}`));\n }\n });\n proc.on('exit', (code, signal) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n const how = signal ? `signal ${signal}` : `code ${code}`;\n reject(\n new Error(`Proxy exited with ${how} before ready${formatStderr()}`),\n );\n }\n });\n });\n}\n\n/**\n * Resolve the proxy endpoint to use for the given Chrome endpoint.\n *\n * - Page-level URLs are auto-resolved to browser-level via /json/version.\n * - If a proxy is already running and connected to the same upstream,\n * reuse it.\n * - If a proxy is running but pointed at a different upstream, kill it\n * and start a fresh one.\n * - If spawning the proxy fails, fall back to the raw Chrome endpoint\n * (the caller will hit Chrome's permission popup directly but at\n * least the command does not fail outright).\n */\nexport async function getProxyEndpoint(\n chromeEndpoint: string,\n): Promise<string> {\n let browserEndpoint = chromeEndpoint;\n if (isPageLevelEndpoint(chromeEndpoint)) {\n debug(\n 'Page-level CDP endpoint detected, resolving via /json/version: %s',\n chromeEndpoint,\n );\n try {\n browserEndpoint = await resolveBrowserEndpoint(chromeEndpoint);\n debug('Resolved browser endpoint: %s', browserEndpoint);\n } catch (err) {\n throw new Error(\n `Cannot use page-level CDP endpoint directly. Puppeteer requires a browser-level endpoint (e.g., ws://host:port/devtools/browser/<id>). Auto-resolution via /json/version failed: ${(err as Error).message}. Please provide a browser-level CDP endpoint instead.`,\n );\n }\n }\n\n if (isProxyAlive()) {\n const endpoint = readProxyEndpoint();\n const savedUpstream = readProxyUpstream();\n if (endpoint) {\n if (savedUpstream && savedUpstream !== browserEndpoint) {\n debug(\n 'Proxy connected to different upstream (%s), killing',\n savedUpstream,\n );\n await killProxy();\n } else {\n return endpoint;\n }\n }\n }\n\n try {\n return await spawnProxy(browserEndpoint);\n } catch (err) {\n console.warn(\n `[cdp] proxy failed, falling back to direct connection: ${err}`,\n );\n return browserEndpoint;\n }\n}\n"],"names":["debug","getDebug","PROXY_TERM_GRACE_MS","PROXY_TERM_POLL_MS","PROXY_STDERR_BUFFER_LIMIT","PROXY_STARTUP_TIMEOUT_MS","isPageLevelEndpoint","endpoint","resolveBrowserEndpoint","pageEndpoint","Promise","resolve","reject","host","url","URL","Error","req","http","res","data","chunk","info","JSON","err","isProxyAlive","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","process","readProxyEndpoint","PROXY_ENDPOINT_FILE","readProxyUpstream","PROXY_UPSTREAM_FILE","sweepProxyMetadataFiles","unlinkSync","killProxy","cleanupTargetIdFile","deadline","Date","r","setTimeout","spawnProxy","chromeEndpoint","proxyScript","join","__dirname","proc","spawn","output","stderrBuf","settled","appendStderr","formatStderr","trimmed","timer","onData","lines","line","parsed","clearTimeout","code","signal","how","getProxyEndpoint","browserEndpoint","savedUpstream","console"],"mappings":"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"}
1
+ {"version":3,"file":"cdp-proxy-manager.mjs","sources":["../../src/cdp-proxy-manager.ts"],"sourcesContent":["/**\n * CDP proxy lifecycle manager (parent-side).\n *\n * Owns everything the CLI needs to start, locate, replace and kill the\n * standalone `cdp-proxy.ts` child process — including its on-disk\n * metadata files (PROXY_ENDPOINT_FILE / PROXY_PID_FILE /\n * PROXY_UPSTREAM_FILE). Also handles auto-resolving page-level CDP URLs\n * to browser-level endpoints via `/json/version`, since puppeteer-core\n * cannot connect to page-level URLs directly.\n *\n * Other modules (notably `agent-tools-cdp.ts`) use this through the\n * `getProxyEndpoint()` entry point and never touch the metadata files\n * themselves. The cross-command targetId is a separate concern owned by\n * `cdp-target-store.ts`.\n */\n\nimport { spawn } from 'node:child_process';\nimport { existsSync, readFileSync, unlinkSync } from 'node:fs';\nimport http from 'node:http';\nimport { join } from 'node:path';\nimport { getDebug } from '@midscene/shared/logger';\nimport {\n PROXY_ENDPOINT_FILE,\n PROXY_PID_FILE,\n PROXY_UPSTREAM_FILE,\n} from './cdp-proxy-constants';\nimport { cleanupTargetIdFile } from './cdp-target-store';\n\nconst debug = getDebug('agent-tools:cdp:proxy');\n\n/** Time to wait for the proxy to exit on SIGTERM before resorting to SIGKILL. */\nconst PROXY_TERM_GRACE_MS = 2000;\n\n/** Polling interval while waiting for the proxy's PID file to disappear. */\nconst PROXY_TERM_POLL_MS = 50;\n\n/** Keep at most this many bytes of proxy stderr for diagnostics. */\nconst PROXY_STDERR_BUFFER_LIMIT = 8 * 1024;\n\n/** How long `spawnProxy()` waits for the child to print its endpoint. */\nconst PROXY_STARTUP_TIMEOUT_MS = 10_000;\n\n/**\n * Page-level CDP URLs (`/devtools/page/<id>`) cannot be passed to\n * `puppeteer.connect()` — puppeteer needs a browser-level endpoint.\n */\nfunction isPageLevelEndpoint(endpoint: string): boolean {\n return /\\/devtools\\/page\\//.test(endpoint);\n}\n\n/**\n * Resolve a page-level CDP URL to its browser-level WebSocket URL by\n * fetching `/json/version` from the same host:port.\n */\nfunction resolveBrowserEndpoint(pageEndpoint: string): Promise<string> {\n return new Promise((resolve, reject) => {\n let host: string;\n try {\n const url = new URL(pageEndpoint);\n host = url.host; // includes port (e.g. \"127.0.0.1:9222\")\n } catch {\n reject(new Error(`Invalid CDP endpoint URL: ${pageEndpoint}`));\n return;\n }\n\n const req = http.get(\n `http://${host}/json/version`,\n { timeout: 5000 },\n (res) => {\n if (res.statusCode && res.statusCode >= 400) {\n reject(new Error(`/json/version returned HTTP ${res.statusCode}`));\n res.resume();\n return;\n }\n let data = '';\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString();\n });\n res.on('end', () => {\n try {\n const info = JSON.parse(data);\n if (info.webSocketDebuggerUrl) {\n resolve(info.webSocketDebuggerUrl);\n } else {\n reject(\n new Error(\n 'webSocketDebuggerUrl not found in /json/version response',\n ),\n );\n }\n } catch {\n reject(\n new Error(`Failed to parse /json/version response: ${data}`),\n );\n }\n });\n },\n );\n req.on('error', (err) =>\n reject(new Error(`Failed to fetch /json/version: ${err.message}`)),\n );\n req.on('timeout', () => {\n req.destroy();\n reject(new Error('Timeout fetching /json/version'));\n });\n });\n}\n\n/**\n * True if the previously spawned proxy process is still running.\n */\nexport function isProxyAlive(): boolean {\n if (!existsSync(PROXY_PID_FILE)) return false;\n try {\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n process.kill(pid, 0); // signal 0 = existence check\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * The local WebSocket URL the running proxy is serving, or null.\n */\nexport function readProxyEndpoint(): string | null {\n if (!existsSync(PROXY_ENDPOINT_FILE)) return null;\n try {\n return readFileSync(PROXY_ENDPOINT_FILE, 'utf-8').trim();\n } catch {\n return null;\n }\n}\n\n/**\n * The Chrome endpoint the running proxy is connected to, or null.\n */\nexport function readProxyUpstream(): string | null {\n if (!existsSync(PROXY_UPSTREAM_FILE)) return null;\n try {\n return readFileSync(PROXY_UPSTREAM_FILE, 'utf-8').trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Best-effort sweep of proxy metadata files. Only used as a fallback\n * when the proxy fails to exit and run its own `cleanupIfOwned()`. In\n * the happy path the child owns these files and removes them itself.\n */\nfunction sweepProxyMetadataFiles(): void {\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_UPSTREAM_FILE)) unlinkSync(PROXY_UPSTREAM_FILE);\n } catch {}\n}\n\n/**\n * Stop the running proxy and discard the cross-command targetId.\n *\n * Sends SIGTERM and waits for the proxy's own SIGTERM handler to remove\n * its PROXY_*_FILE metadata via `cleanupIfOwned()`. When the PID file\n * disappears we know the next `spawnProxy()` can safely take over\n * without tripping the duplicate-proxy guard. Falls back to SIGKILL +\n * manual sweep if the proxy is unresponsive within `PROXY_TERM_GRACE_MS`.\n *\n * The targetId file is cleared regardless of the proxy's state because\n * it points into the outgoing Chrome's tab list.\n */\nexport async function killProxy(): Promise<void> {\n cleanupTargetIdFile();\n\n if (!existsSync(PROXY_PID_FILE)) return;\n\n let pid: number;\n try {\n pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (!Number.isFinite(pid)) {\n sweepProxyMetadataFiles();\n return;\n }\n } catch (err) {\n debug('killProxy: cannot read pid file: %s', err);\n sweepProxyMetadataFiles();\n return;\n }\n\n try {\n process.kill(pid, 'SIGTERM');\n debug('Sent SIGTERM to proxy pid %d', pid);\n } catch (err) {\n // ESRCH (already dead) is the common case; surface anything else\n // (e.g. EPERM) via debug so it does not vanish silently. Either way\n // sweep the orphan metadata so the next spawn has a clean slate.\n debug('killProxy: SIGTERM failed (pid %d): %s', pid, err);\n sweepProxyMetadataFiles();\n return;\n }\n\n const deadline = Date.now() + PROXY_TERM_GRACE_MS;\n while (Date.now() < deadline && existsSync(PROXY_PID_FILE)) {\n await new Promise((r) => setTimeout(r, PROXY_TERM_POLL_MS));\n }\n\n if (existsSync(PROXY_PID_FILE)) {\n debug(\n 'proxy pid %d did not clean up within %dms, forcing SIGKILL',\n pid,\n PROXY_TERM_GRACE_MS,\n );\n try {\n process.kill(pid, 'SIGKILL');\n } catch {}\n sweepProxyMetadataFiles();\n }\n}\n\n/**\n * Spawn the CDP proxy process and wait for it to print the endpoint.\n *\n * Captures the child's stderr so that when startup fails we can surface\n * the real reason (upstream closed / duplicate proxy / upstream error)\n * instead of the generic \"exited before ready\".\n */\nfunction spawnProxy(chromeEndpoint: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const proxyScript = join(__dirname, 'cdp-proxy.js');\n const proc = spawn(process.execPath, [proxyScript, chromeEndpoint], {\n detached: true,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n proc.unref();\n\n let output = '';\n let stderrBuf = '';\n let settled = false;\n\n const appendStderr = (chunk: Buffer) => {\n stderrBuf += chunk.toString();\n if (stderrBuf.length > PROXY_STDERR_BUFFER_LIMIT) {\n stderrBuf = stderrBuf.slice(-PROXY_STDERR_BUFFER_LIMIT);\n }\n };\n proc.stderr!.on('data', appendStderr);\n\n const formatStderr = () => {\n const trimmed = stderrBuf.trim();\n return trimmed ? ` (stderr: ${trimmed})` : '';\n };\n\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(\n new Error(\n `Proxy startup timeout (${PROXY_STARTUP_TIMEOUT_MS / 1000}s)${formatStderr()}`,\n ),\n );\n }\n }, PROXY_STARTUP_TIMEOUT_MS);\n\n const onData = (chunk: Buffer) => {\n output += chunk.toString();\n const lines = output.split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line);\n if (parsed.endpoint && !settled) {\n settled = true;\n clearTimeout(timer);\n proc.stdout!.removeListener('data', onData);\n proc.stderr!.removeListener('data', appendStderr);\n // Destroy the stdio pipes so they don't keep the parent\n // process event loop alive after we've read the endpoint.\n proc.stdout!.destroy();\n proc.stderr!.destroy();\n resolve(parsed.endpoint);\n return;\n }\n } catch {\n // stdout may contain non-JSON lines during startup — skip them\n }\n }\n };\n proc.stdout!.on('data', onData);\n\n proc.on('error', (err) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n reject(new Error(`Failed to spawn proxy: ${err.message}`));\n }\n });\n proc.on('exit', (code, signal) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n const how = signal ? `signal ${signal}` : `code ${code}`;\n reject(\n new Error(`Proxy exited with ${how} before ready${formatStderr()}`),\n );\n }\n });\n });\n}\n\n/**\n * Resolve the proxy endpoint to use for the given Chrome endpoint.\n *\n * - Page-level URLs are auto-resolved to browser-level via /json/version.\n * - If a proxy is already running and connected to the same upstream,\n * reuse it.\n * - If a proxy is running but pointed at a different upstream, kill it\n * and start a fresh one.\n * - If spawning the proxy fails, fall back to the raw Chrome endpoint\n * (the caller will hit Chrome's permission popup directly but at\n * least the command does not fail outright).\n */\nexport async function getProxyEndpoint(\n chromeEndpoint: string,\n): Promise<string> {\n let browserEndpoint = chromeEndpoint;\n if (isPageLevelEndpoint(chromeEndpoint)) {\n debug(\n 'Page-level CDP endpoint detected, resolving via /json/version: %s',\n chromeEndpoint,\n );\n try {\n browserEndpoint = await resolveBrowserEndpoint(chromeEndpoint);\n debug('Resolved browser endpoint: %s', browserEndpoint);\n } catch (err) {\n throw new Error(\n `Cannot use page-level CDP endpoint directly. Puppeteer requires a browser-level endpoint (e.g., ws://host:port/devtools/browser/<id>). Auto-resolution via /json/version failed: ${(err as Error).message}. Please provide a browser-level CDP endpoint instead.`,\n );\n }\n }\n\n if (isProxyAlive()) {\n const endpoint = readProxyEndpoint();\n const savedUpstream = readProxyUpstream();\n if (endpoint) {\n if (savedUpstream && savedUpstream !== browserEndpoint) {\n debug(\n 'Proxy connected to different upstream (%s), killing',\n savedUpstream,\n );\n await killProxy();\n } else {\n return endpoint;\n }\n }\n }\n\n try {\n return await spawnProxy(browserEndpoint);\n } catch (err) {\n console.warn(\n `[cdp] proxy failed, falling back to direct connection: ${err}`,\n );\n return browserEndpoint;\n }\n}\n"],"names":["debug","getDebug","PROXY_TERM_GRACE_MS","PROXY_TERM_POLL_MS","PROXY_STDERR_BUFFER_LIMIT","PROXY_STARTUP_TIMEOUT_MS","isPageLevelEndpoint","endpoint","resolveBrowserEndpoint","pageEndpoint","Promise","resolve","reject","host","url","URL","Error","req","http","res","data","chunk","info","JSON","err","isProxyAlive","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","process","readProxyEndpoint","PROXY_ENDPOINT_FILE","readProxyUpstream","PROXY_UPSTREAM_FILE","sweepProxyMetadataFiles","unlinkSync","killProxy","cleanupTargetIdFile","deadline","Date","r","setTimeout","spawnProxy","chromeEndpoint","proxyScript","join","__dirname","proc","spawn","output","stderrBuf","settled","appendStderr","formatStderr","trimmed","timer","onData","lines","line","parsed","clearTimeout","code","signal","how","getProxyEndpoint","browserEndpoint","savedUpstream","console"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-proxy.mjs","sources":["../../src/cdp-proxy.ts"],"sourcesContent":["/**\n * CDP WebSocket Proxy — standalone process.\n *\n * Holds a persistent WebSocket connection to Chrome's CDP endpoint and\n * exposes a local WebSocket server. Midscene CLI processes connect to the\n * proxy instead of Chrome directly, so Chrome's \"Allow remote debugging\"\n * permission popup only fires once (when the proxy connects).\n *\n * Lifecycle notes:\n * - When all downstream clients disconnect the proxy stays running but\n * marks the upstream as needing reconnection. The actual reconnect is\n * deferred to the moment the next client connects, so Chrome's CDP\n * state (notably Target.setDiscoverTargets) is reset and the new\n * client receives all targetCreated events.\n * - On startup, if another proxy is already alive the new instance\n * announces \"duplicate proxy detected\" on stderr and exits 0 without\n * touching the existing metadata files.\n *\n * Exit conditions:\n * 1. Upstream Chrome connection closes or errors.\n * 2. No downstream client message for IDLE_TIMEOUT_MS (default 5 min).\n * 3. SIGTERM / SIGINT.\n * 4. Duplicate proxy detected on startup (exits 0 with stderr notice).\n *\n * Usage (spawned by mcp-tools-cdp.ts):\n * node cdp-proxy.js <chrome-ws-endpoint>\n *\n * On startup, prints the proxy endpoint to stdout as a single JSON line:\n * {\"endpoint\":\"ws://127.0.0.1:<port>/devtools/browser\"}\n * and writes:\n * - PROXY_ENDPOINT_FILE — the local proxy URL above\n * - PROXY_PID_FILE — this process's pid\n * - PROXY_UPSTREAM_FILE — the Chrome endpoint the proxy is connected to,\n * so callers can detect when the requested\n * upstream has changed and replace the proxy.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n PROXY_ENDPOINT_FILE,\n PROXY_PID_FILE,\n PROXY_UPSTREAM_FILE,\n} from './cdp-proxy-constants';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\nconst chromeEndpoint = process.argv[2];\nif (!chromeEndpoint) {\n process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\\n');\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Cleanup\n// ---------------------------------------------------------------------------\n\nfunction cleanupIfOwned() {\n // Only clean up if the PID file exists and records *our* PID.\n // If the file is missing (e.g. already deleted by killProxy()) or\n // unreadable, skip cleanup — another process may have taken over.\n try {\n if (!existsSync(PROXY_PID_FILE)) return;\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (pid !== process.pid) return;\n } catch {\n return;\n }\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_UPSTREAM_FILE)) unlinkSync(PROXY_UPSTREAM_FILE);\n } catch {}\n}\n\n/**\n * Maximum time to wait for the stderr drain callback before forcing exit.\n * The callback should normally fire within microseconds, but if the pipe\n * has been closed by the parent it may never run. 500ms is generous\n * enough to be effectively unreachable in the happy path while still\n * keeping the process from hanging if something goes wrong.\n */\nconst STDERR_FLUSH_FALLBACK_MS = 500;\n\n/**\n * Exit after the stderr diagnostic has been flushed.\n *\n * When the proxy's stderr is a pipe (parent uses stdio 'pipe'), Node's\n * process.stderr.write() is asynchronous on POSIX. Calling process.exit()\n * immediately afterwards can drop the pending write, which would silently\n * lose the very diagnostic the caller is relying on. Wait for the drain\n * callback before exiting, with a short fallback timer in case the\n * callback never fires (e.g. the pipe is already closed).\n */\nfunction exitWithStderr(message: string, code = 0): void {\n let exited = false;\n const doExit = () => {\n if (exited) return;\n exited = true;\n process.exit(code);\n };\n const fallback = setTimeout(doExit, STDERR_FLUSH_FALLBACK_MS);\n fallback.unref?.();\n try {\n process.stderr.write(message, () => doExit());\n } catch {\n doExit();\n }\n}\n\nfunction shutdown(reason: string): void {\n cleanupIfOwned();\n exitWithStderr(`[cdp-proxy] shutting down: ${reason}\\n`, 0);\n}\n\nprocess.on('SIGTERM', () => shutdown('SIGTERM'));\nprocess.on('SIGINT', () => shutdown('SIGINT'));\nprocess.on('uncaughtException', (e) => shutdown(`uncaught: ${e.message}`));\n\n// ---------------------------------------------------------------------------\n// Idle timer\n// ---------------------------------------------------------------------------\n\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction resetIdleTimer() {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(\n () => shutdown('idle timeout (5min)'),\n IDLE_TIMEOUT_MS,\n );\n}\n\nresetIdleTimer();\n\n// ---------------------------------------------------------------------------\n// Upstream: connect to Chrome\n// ---------------------------------------------------------------------------\n\nconst clients = new Set<WebSocket>();\n\n/**\n * Whether we are intentionally reconnecting the upstream WebSocket.\n * When true, the old upstream's close/error events should not trigger shutdown.\n */\nlet reconnecting = false;\n\n/**\n * Whether the upstream WebSocket needs to be reconnected before the next\n * client can use it. Set to true when all downstream clients disconnect;\n * the actual reconnect is deferred until a new client connects so that\n * Chrome's permission popup only fires when someone actually needs it.\n */\nlet needsUpstreamReconnect = false;\n\n/**\n * Messages from downstream clients that arrived while the upstream WebSocket\n * was not yet open (e.g. during a reconnect). Flushed once upstream opens.\n */\nconst pendingUpstreamMessages: {\n data: WebSocket.RawData;\n isBinary: boolean;\n}[] = [];\n\n/**\n * Create a new upstream WebSocket to Chrome and bind its event handlers.\n * Used for both initial connection and reconnection.\n */\nfunction createUpstream(endpoint: string): WebSocket {\n const ws = new WebSocket(endpoint);\n\n ws.on('error', (err) => {\n // A failed reconnect must not leave the flag stuck on.\n reconnecting = false;\n shutdown(`upstream error: ${err.message}`);\n });\n\n ws.on('close', (code, reasonBuf) => {\n if (reconnecting) return;\n const reason = reasonBuf?.toString?.() || '';\n const detail = reason\n ? ` (code=${code}, reason=${reason})`\n : ` (code=${code})`;\n shutdown(`upstream closed${detail}`);\n });\n\n // Forward upstream messages to all downstream clients\n ws.on('message', (data, isBinary) => {\n resetIdleTimer();\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n }\n });\n\n ws.on('open', () => {\n reconnecting = false;\n for (const msg of pendingUpstreamMessages) {\n ws.send(msg.data, { binary: msg.isBinary });\n }\n pendingUpstreamMessages.length = 0;\n });\n\n return ws;\n}\n\nlet upstream = createUpstream(chromeEndpoint);\n\n/**\n * Reconnect the upstream WebSocket to Chrome.\n *\n * Called when all downstream clients have disconnected. This resets the CDP\n * protocol state on Chrome's side — critically, the Target.setDiscoverTargets\n * subscription — so the next client that connects gets a fresh session and\n * receives all targetCreated events.\n *\n * `reconnecting` is cleared by the new upstream's `open` (or `error`) handler,\n * not here — the new socket is still CONNECTING when this returns.\n */\nfunction reconnectUpstream() {\n reconnecting = true;\n upstream.removeAllListeners();\n upstream.close();\n upstream = createUpstream(chromeEndpoint);\n resetIdleTimer();\n}\n\n// ---------------------------------------------------------------------------\n// Downstream: local WebSocket server\n// ---------------------------------------------------------------------------\n\nconst httpServer = createServer((_req, res) => {\n res.writeHead(404);\n res.end();\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\n\nwss.on('connection', (clientWs) => {\n // Reconnect the upstream WebSocket if the previous session ended.\n // This resets Chrome's CDP protocol state (Target.setDiscoverTargets, etc.)\n // so the new client receives all targetCreated events.\n if (needsUpstreamReconnect && upstream.readyState === WebSocket.OPEN) {\n reconnectUpstream();\n needsUpstreamReconnect = false;\n }\n\n clients.add(clientWs);\n resetIdleTimer();\n\n // Forward downstream messages to upstream (buffer if upstream is reconnecting)\n clientWs.on('message', (data, isBinary) => {\n resetIdleTimer();\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n } else {\n pendingUpstreamMessages.push({ data, isBinary });\n }\n });\n\n const removeClient = () => {\n clients.delete(clientWs);\n // When all downstream clients disconnect, mark that the upstream needs\n // reconnecting to reset Chrome's CDP protocol state. The actual\n // reconnect is deferred until the next client connects so we don't\n // trigger Chrome's permission popup while nobody is using the proxy.\n if (clients.size === 0) {\n pendingUpstreamMessages.length = 0;\n needsUpstreamReconnect = true;\n }\n };\n\n clientWs.on('close', removeClient);\n clientWs.on('error', removeClient);\n});\n\n// ---------------------------------------------------------------------------\n// Start\n// ---------------------------------------------------------------------------\n\n// Start the HTTP/WebSocket server once the initial upstream connection opens.\n// This listener is added *after* createUpstream() already bound its own 'open'\n// handler (which flushes pendingUpstreamMessages), so both will fire.\nupstream.once('open', () => {\n // Check for duplicate proxy\n if (existsSync(PROXY_PID_FILE)) {\n try {\n const existingPid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (existingPid !== process.pid) {\n try {\n process.kill(existingPid, 0);\n // Another proxy is alive — exit without cleanupIfOwned() (we don't\n // own the metadata files). Announce the reason on stderr so the\n // parent process can distinguish this path from upstream failures,\n // then bail out of this open handler so we don't proceed to listen().\n exitWithStderr(\n `[cdp-proxy] duplicate proxy detected (existing pid=${existingPid})\\n`,\n 0,\n );\n return;\n } catch {\n // dead — we take over\n }\n }\n } catch {}\n }\n\n httpServer.listen(0, '127.0.0.1', () => {\n const addr = httpServer.address();\n if (!addr || typeof addr === 'string') {\n shutdown('failed to get server address');\n return;\n }\n\n const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;\n\n writeFileSync(PROXY_ENDPOINT_FILE, proxyEndpoint);\n writeFileSync(PROXY_PID_FILE, String(process.pid));\n writeFileSync(PROXY_UPSTREAM_FILE, chromeEndpoint);\n\n process.stdout.write(`${JSON.stringify({ endpoint: proxyEndpoint })}\\n`);\n });\n});\n"],"names":["IDLE_TIMEOUT_MS","chromeEndpoint","process","cleanupIfOwned","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","PROXY_ENDPOINT_FILE","unlinkSync","PROXY_UPSTREAM_FILE","STDERR_FLUSH_FALLBACK_MS","exitWithStderr","message","code","exited","doExit","fallback","setTimeout","shutdown","reason","e","idleTimer","resetIdleTimer","clearTimeout","clients","Set","reconnecting","needsUpstreamReconnect","pendingUpstreamMessages","createUpstream","endpoint","ws","WebSocket","err","reasonBuf","detail","data","isBinary","client","msg","upstream","reconnectUpstream","httpServer","createServer","_req","res","wss","WebSocketServer","clientWs","removeClient","existingPid","addr","proxyEndpoint","writeFileSync","String","JSON"],"mappings":"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"}
1
+ {"version":3,"file":"cdp-proxy.mjs","sources":["../../src/cdp-proxy.ts"],"sourcesContent":["/**\n * CDP WebSocket Proxy — standalone process.\n *\n * Holds a persistent WebSocket connection to Chrome's CDP endpoint and\n * exposes a local WebSocket server. Midscene CLI processes connect to the\n * proxy instead of Chrome directly, so Chrome's \"Allow remote debugging\"\n * permission popup only fires once (when the proxy connects).\n *\n * Lifecycle notes:\n * - When all downstream clients disconnect the proxy stays running but\n * marks the upstream as needing reconnection. The actual reconnect is\n * deferred to the moment the next client connects, so Chrome's CDP\n * state (notably Target.setDiscoverTargets) is reset and the new\n * client receives all targetCreated events.\n * - On startup, if another proxy is already alive the new instance\n * announces \"duplicate proxy detected\" on stderr and exits 0 without\n * touching the existing metadata files.\n *\n * Exit conditions:\n * 1. Upstream Chrome connection closes or errors.\n * 2. No downstream client message for IDLE_TIMEOUT_MS (default 5 min).\n * 3. SIGTERM / SIGINT.\n * 4. Duplicate proxy detected on startup (exits 0 with stderr notice).\n *\n * Usage (spawned by agent-tools-cdp.ts):\n * node cdp-proxy.js <chrome-ws-endpoint>\n *\n * On startup, prints the proxy endpoint to stdout as a single JSON line:\n * {\"endpoint\":\"ws://127.0.0.1:<port>/devtools/browser\"}\n * and writes:\n * - PROXY_ENDPOINT_FILE — the local proxy URL above\n * - PROXY_PID_FILE — this process's pid\n * - PROXY_UPSTREAM_FILE — the Chrome endpoint the proxy is connected to,\n * so callers can detect when the requested\n * upstream has changed and replace the proxy.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport {\n PROXY_ENDPOINT_FILE,\n PROXY_PID_FILE,\n PROXY_UPSTREAM_FILE,\n} from './cdp-proxy-constants';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\nconst chromeEndpoint = process.argv[2];\nif (!chromeEndpoint) {\n process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\\n');\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Cleanup\n// ---------------------------------------------------------------------------\n\nfunction cleanupIfOwned() {\n // Only clean up if the PID file exists and records *our* PID.\n // If the file is missing (e.g. already deleted by killProxy()) or\n // unreadable, skip cleanup — another process may have taken over.\n try {\n if (!existsSync(PROXY_PID_FILE)) return;\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (pid !== process.pid) return;\n } catch {\n return;\n }\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_UPSTREAM_FILE)) unlinkSync(PROXY_UPSTREAM_FILE);\n } catch {}\n}\n\n/**\n * Maximum time to wait for the stderr drain callback before forcing exit.\n * The callback should normally fire within microseconds, but if the pipe\n * has been closed by the parent it may never run. 500ms is generous\n * enough to be effectively unreachable in the happy path while still\n * keeping the process from hanging if something goes wrong.\n */\nconst STDERR_FLUSH_FALLBACK_MS = 500;\n\n/**\n * Exit after the stderr diagnostic has been flushed.\n *\n * When the proxy's stderr is a pipe (parent uses stdio 'pipe'), Node's\n * process.stderr.write() is asynchronous on POSIX. Calling process.exit()\n * immediately afterwards can drop the pending write, which would silently\n * lose the very diagnostic the caller is relying on. Wait for the drain\n * callback before exiting, with a short fallback timer in case the\n * callback never fires (e.g. the pipe is already closed).\n */\nfunction exitWithStderr(message: string, code = 0): void {\n let exited = false;\n const doExit = () => {\n if (exited) return;\n exited = true;\n process.exit(code);\n };\n const fallback = setTimeout(doExit, STDERR_FLUSH_FALLBACK_MS);\n fallback.unref?.();\n try {\n process.stderr.write(message, () => doExit());\n } catch {\n doExit();\n }\n}\n\nfunction shutdown(reason: string): void {\n cleanupIfOwned();\n exitWithStderr(`[cdp-proxy] shutting down: ${reason}\\n`, 0);\n}\n\nprocess.on('SIGTERM', () => shutdown('SIGTERM'));\nprocess.on('SIGINT', () => shutdown('SIGINT'));\nprocess.on('uncaughtException', (e) => shutdown(`uncaught: ${e.message}`));\n\n// ---------------------------------------------------------------------------\n// Idle timer\n// ---------------------------------------------------------------------------\n\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction resetIdleTimer() {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(\n () => shutdown('idle timeout (5min)'),\n IDLE_TIMEOUT_MS,\n );\n}\n\nresetIdleTimer();\n\n// ---------------------------------------------------------------------------\n// Upstream: connect to Chrome\n// ---------------------------------------------------------------------------\n\nconst clients = new Set<WebSocket>();\n\n/**\n * Whether we are intentionally reconnecting the upstream WebSocket.\n * When true, the old upstream's close/error events should not trigger shutdown.\n */\nlet reconnecting = false;\n\n/**\n * Whether the upstream WebSocket needs to be reconnected before the next\n * client can use it. Set to true when all downstream clients disconnect;\n * the actual reconnect is deferred until a new client connects so that\n * Chrome's permission popup only fires when someone actually needs it.\n */\nlet needsUpstreamReconnect = false;\n\n/**\n * Messages from downstream clients that arrived while the upstream WebSocket\n * was not yet open (e.g. during a reconnect). Flushed once upstream opens.\n */\nconst pendingUpstreamMessages: {\n data: WebSocket.RawData;\n isBinary: boolean;\n}[] = [];\n\n/**\n * Create a new upstream WebSocket to Chrome and bind its event handlers.\n * Used for both initial connection and reconnection.\n */\nfunction createUpstream(endpoint: string): WebSocket {\n const ws = new WebSocket(endpoint);\n\n ws.on('error', (err) => {\n // A failed reconnect must not leave the flag stuck on.\n reconnecting = false;\n shutdown(`upstream error: ${err.message}`);\n });\n\n ws.on('close', (code, reasonBuf) => {\n if (reconnecting) return;\n const reason = reasonBuf?.toString?.() || '';\n const detail = reason\n ? ` (code=${code}, reason=${reason})`\n : ` (code=${code})`;\n shutdown(`upstream closed${detail}`);\n });\n\n // Forward upstream messages to all downstream clients\n ws.on('message', (data, isBinary) => {\n resetIdleTimer();\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n }\n });\n\n ws.on('open', () => {\n reconnecting = false;\n for (const msg of pendingUpstreamMessages) {\n ws.send(msg.data, { binary: msg.isBinary });\n }\n pendingUpstreamMessages.length = 0;\n });\n\n return ws;\n}\n\nlet upstream = createUpstream(chromeEndpoint);\n\n/**\n * Reconnect the upstream WebSocket to Chrome.\n *\n * Called when all downstream clients have disconnected. This resets the CDP\n * protocol state on Chrome's side — critically, the Target.setDiscoverTargets\n * subscription — so the next client that connects gets a fresh session and\n * receives all targetCreated events.\n *\n * `reconnecting` is cleared by the new upstream's `open` (or `error`) handler,\n * not here — the new socket is still CONNECTING when this returns.\n */\nfunction reconnectUpstream() {\n reconnecting = true;\n upstream.removeAllListeners();\n upstream.close();\n upstream = createUpstream(chromeEndpoint);\n resetIdleTimer();\n}\n\n// ---------------------------------------------------------------------------\n// Downstream: local WebSocket server\n// ---------------------------------------------------------------------------\n\nconst httpServer = createServer((_req, res) => {\n res.writeHead(404);\n res.end();\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\n\nwss.on('connection', (clientWs) => {\n // Reconnect the upstream WebSocket if the previous session ended.\n // This resets Chrome's CDP protocol state (Target.setDiscoverTargets, etc.)\n // so the new client receives all targetCreated events.\n if (needsUpstreamReconnect && upstream.readyState === WebSocket.OPEN) {\n reconnectUpstream();\n needsUpstreamReconnect = false;\n }\n\n clients.add(clientWs);\n resetIdleTimer();\n\n // Forward downstream messages to upstream (buffer if upstream is reconnecting)\n clientWs.on('message', (data, isBinary) => {\n resetIdleTimer();\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n } else {\n pendingUpstreamMessages.push({ data, isBinary });\n }\n });\n\n const removeClient = () => {\n clients.delete(clientWs);\n // When all downstream clients disconnect, mark that the upstream needs\n // reconnecting to reset Chrome's CDP protocol state. The actual\n // reconnect is deferred until the next client connects so we don't\n // trigger Chrome's permission popup while nobody is using the proxy.\n if (clients.size === 0) {\n pendingUpstreamMessages.length = 0;\n needsUpstreamReconnect = true;\n }\n };\n\n clientWs.on('close', removeClient);\n clientWs.on('error', removeClient);\n});\n\n// ---------------------------------------------------------------------------\n// Start\n// ---------------------------------------------------------------------------\n\n// Start the HTTP/WebSocket server once the initial upstream connection opens.\n// This listener is added *after* createUpstream() already bound its own 'open'\n// handler (which flushes pendingUpstreamMessages), so both will fire.\nupstream.once('open', () => {\n // Check for duplicate proxy\n if (existsSync(PROXY_PID_FILE)) {\n try {\n const existingPid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (existingPid !== process.pid) {\n try {\n process.kill(existingPid, 0);\n // Another proxy is alive — exit without cleanupIfOwned() (we don't\n // own the metadata files). Announce the reason on stderr so the\n // parent process can distinguish this path from upstream failures,\n // then bail out of this open handler so we don't proceed to listen().\n exitWithStderr(\n `[cdp-proxy] duplicate proxy detected (existing pid=${existingPid})\\n`,\n 0,\n );\n return;\n } catch {\n // dead — we take over\n }\n }\n } catch {}\n }\n\n httpServer.listen(0, '127.0.0.1', () => {\n const addr = httpServer.address();\n if (!addr || typeof addr === 'string') {\n shutdown('failed to get server address');\n return;\n }\n\n const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;\n\n writeFileSync(PROXY_ENDPOINT_FILE, proxyEndpoint);\n writeFileSync(PROXY_PID_FILE, String(process.pid));\n writeFileSync(PROXY_UPSTREAM_FILE, chromeEndpoint);\n\n process.stdout.write(`${JSON.stringify({ endpoint: proxyEndpoint })}\\n`);\n });\n});\n"],"names":["IDLE_TIMEOUT_MS","chromeEndpoint","process","cleanupIfOwned","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","PROXY_ENDPOINT_FILE","unlinkSync","PROXY_UPSTREAM_FILE","STDERR_FLUSH_FALLBACK_MS","exitWithStderr","message","code","exited","doExit","fallback","setTimeout","shutdown","reason","e","idleTimer","resetIdleTimer","clearTimeout","clients","Set","reconnecting","needsUpstreamReconnect","pendingUpstreamMessages","createUpstream","endpoint","ws","WebSocket","err","reasonBuf","detail","data","isBinary","client","msg","upstream","reconnectUpstream","httpServer","createServer","_req","res","wss","WebSocketServer","clientWs","removeClient","existingPid","addr","proxyEndpoint","writeFileSync","String","JSON"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { getDebug } from "@midscene/shared/logger";
3
3
  import { TARGET_ID_FILE } from "./cdp-proxy-constants.mjs";
4
- const debug = getDebug('mcp:cdp:target-store');
4
+ const debug = getDebug('agent-tools:cdp:target-store');
5
5
  function readSavedTargetId() {
6
6
  if (!existsSync(TARGET_ID_FILE)) return null;
7
7
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"cdp-target-store.mjs","sources":["../../src/cdp-target-store.ts"],"sourcesContent":["/**\n * Persistent store for the CDP-mode \"current tab\" targetId.\n *\n * The Midscene CLI runs each command as a fresh Node process, so anything\n * the previous command knew about which tab was being driven must survive\n * across processes. This store writes the chosen targetId to a temp file\n * after `connect`/`act`/etc. succeed, and the next command reads it back\n * to bind to the exact same tab — even when Chrome holds 14 of them.\n *\n * Owns nothing else. The CDP proxy lifecycle and its own metadata files\n * live in `cdp-proxy-manager.ts`.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { getDebug } from '@midscene/shared/logger';\nimport { TARGET_ID_FILE } from './cdp-proxy-constants';\n\nconst debug = getDebug('mcp:cdp:target-store');\n\n/**\n * Read the saved targetId, or null if no command has stored one yet.\n */\nexport function readSavedTargetId(): string | null {\n if (!existsSync(TARGET_ID_FILE)) return null;\n try {\n return readFileSync(TARGET_ID_FILE, 'utf-8').trim() || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a targetId so the next CLI command can rebind to the same tab.\n */\nexport function saveTargetId(targetId: string): void {\n try {\n writeFileSync(TARGET_ID_FILE, targetId, 'utf-8');\n debug('Saved targetId: %s', targetId);\n } catch (err) {\n debug('Failed to save targetId: %s', err);\n }\n}\n\n/**\n * Discard the saved targetId — call when disconnecting or when the\n * upstream Chrome changes (the targetId would point into the old\n * browser's tab list).\n */\nexport function cleanupTargetIdFile(): void {\n try {\n if (existsSync(TARGET_ID_FILE)) unlinkSync(TARGET_ID_FILE);\n } catch {}\n}\n"],"names":["debug","getDebug","readSavedTargetId","existsSync","TARGET_ID_FILE","readFileSync","saveTargetId","targetId","writeFileSync","err","cleanupTargetIdFile","unlinkSync"],"mappings":";;;AAiBA,MAAMA,QAAQC,SAAS;AAKhB,SAASC;IACd,IAAI,CAACC,WAAWC,iBAAiB,OAAO;IACxC,IAAI;QACF,OAAOC,aAAaD,gBAAgB,SAAS,IAAI,MAAM;IACzD,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAKO,SAASE,aAAaC,QAAgB;IAC3C,IAAI;QACFC,cAAcJ,gBAAgBG,UAAU;QACxCP,MAAM,sBAAsBO;IAC9B,EAAE,OAAOE,KAAK;QACZT,MAAM,+BAA+BS;IACvC;AACF;AAOO,SAASC;IACd,IAAI;QACF,IAAIP,WAAWC,iBAAiBO,WAAWP;IAC7C,EAAE,OAAM,CAAC;AACX"}
1
+ {"version":3,"file":"cdp-target-store.mjs","sources":["../../src/cdp-target-store.ts"],"sourcesContent":["/**\n * Persistent store for the CDP-mode \"current tab\" targetId.\n *\n * The Midscene CLI runs each command as a fresh Node process, so anything\n * the previous command knew about which tab was being driven must survive\n * across processes. This store writes the chosen targetId to a temp file\n * after `connect`/`act`/etc. succeed, and the next command reads it back\n * to bind to the exact same tab — even when Chrome holds 14 of them.\n *\n * Owns nothing else. The CDP proxy lifecycle and its own metadata files\n * live in `cdp-proxy-manager.ts`.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { getDebug } from '@midscene/shared/logger';\nimport { TARGET_ID_FILE } from './cdp-proxy-constants';\n\nconst debug = getDebug('agent-tools:cdp:target-store');\n\n/**\n * Read the saved targetId, or null if no command has stored one yet.\n */\nexport function readSavedTargetId(): string | null {\n if (!existsSync(TARGET_ID_FILE)) return null;\n try {\n return readFileSync(TARGET_ID_FILE, 'utf-8').trim() || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Save a targetId so the next CLI command can rebind to the same tab.\n */\nexport function saveTargetId(targetId: string): void {\n try {\n writeFileSync(TARGET_ID_FILE, targetId, 'utf-8');\n debug('Saved targetId: %s', targetId);\n } catch (err) {\n debug('Failed to save targetId: %s', err);\n }\n}\n\n/**\n * Discard the saved targetId — call when disconnecting or when the\n * upstream Chrome changes (the targetId would point into the old\n * browser's tab list).\n */\nexport function cleanupTargetIdFile(): void {\n try {\n if (existsSync(TARGET_ID_FILE)) unlinkSync(TARGET_ID_FILE);\n } catch {}\n}\n"],"names":["debug","getDebug","readSavedTargetId","existsSync","TARGET_ID_FILE","readFileSync","saveTargetId","targetId","writeFileSync","err","cleanupTargetIdFile","unlinkSync"],"mappings":";;;AAiBA,MAAMA,QAAQC,SAAS;AAKhB,SAASC;IACd,IAAI,CAACC,WAAWC,iBAAiB,OAAO;IACxC,IAAI;QACF,OAAOC,aAAaD,gBAAgB,SAAS,IAAI,MAAM;IACzD,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAKO,SAASE,aAAaC,QAAgB;IAC3C,IAAI;QACFC,cAAcJ,gBAAgBG,UAAU;QACxCP,MAAM,sBAAsBO;IAC9B,EAAE,OAAOE,KAAK;QACZT,MAAM,+BAA+BS;IACvC;AACF;AAOO,SAASC;IACd,IAAI;QACF,IAAIP,WAAWC,iBAAiBO,WAAWP;IAC7C,EAAE,OAAM,CAAC;AACX"}
package/dist/es/cli.mjs CHANGED
@@ -3,10 +3,10 @@ import { join } from "node:path";
3
3
  import { createReportCliCommands } from "@midscene/core";
4
4
  import { reportCLIError, runToolsCLI } from "@midscene/shared/cli";
5
5
  import dotenv from "dotenv";
6
+ import { WebMidsceneTools } from "./agent-tools.mjs";
7
+ import { WebCdpMidsceneTools } from "./agent-tools-cdp.mjs";
8
+ import { WebPuppeteerMidsceneTools } from "./agent-tools-puppeteer.mjs";
6
9
  import { parseWebCliOptions } from "./cli-options.mjs";
7
- import { WebMidsceneTools } from "./mcp-tools.mjs";
8
- import { WebCdpMidsceneTools } from "./mcp-tools-cdp.mjs";
9
- import { WebPuppeteerMidsceneTools } from "./mcp-tools-puppeteer.mjs";
10
10
  const envFile = join(process.cwd(), '.env');
11
11
  if (existsSync(envFile)) dotenv.config({
12
12
  path: envFile
@@ -18,7 +18,7 @@ Promise.resolve().then(()=>{
18
18
  return runToolsCLI(tools, 'midscene-web', {
19
19
  stripPrefix: 'web_',
20
20
  argv: parsedOptions.argv,
21
- version: "1.9.7",
21
+ version: "1.9.8-beta-20260618014851.0",
22
22
  extraCommands: createReportCliCommands()
23
23
  });
24
24
  }).catch((e)=>{
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","sources":["../../src/cli.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createReportCliCommands } from '@midscene/core';\nimport { reportCLIError, runToolsCLI } from '@midscene/shared/cli';\nimport dotenv from 'dotenv';\nimport { parseWebCliOptions } from './cli-options';\nimport { WebMidsceneTools } from './mcp-tools';\nimport { WebCdpMidsceneTools } from './mcp-tools-cdp';\nimport { WebPuppeteerMidsceneTools } from './mcp-tools-puppeteer';\n\n// Load .env early so MIDSCENE_CDP_ENDPOINT is available during arg parsing\nconst envFile = join(process.cwd(), '.env');\nif (existsSync(envFile)) {\n dotenv.config({ path: envFile });\n}\n\ndeclare const __VERSION__: string;\n\nPromise.resolve()\n .then(() => {\n const parsedOptions = parseWebCliOptions(process.argv.slice(2));\n\n let tools:\n | WebMidsceneTools\n | WebPuppeteerMidsceneTools\n | WebCdpMidsceneTools;\n if (parsedOptions.mode === 'bridge') {\n tools = new WebMidsceneTools();\n } else if (parsedOptions.mode === 'cdp') {\n tools = new WebCdpMidsceneTools(parsedOptions.cdpEndpoint!);\n } else {\n tools = new WebPuppeteerMidsceneTools(parsedOptions.viewport);\n }\n\n return runToolsCLI(tools, 'midscene-web', {\n stripPrefix: 'web_',\n argv: parsedOptions.argv,\n version: __VERSION__,\n extraCommands: createReportCliCommands(),\n });\n })\n .catch((e) => {\n process.exit(reportCLIError(e));\n });\n"],"names":["envFile","join","process","existsSync","dotenv","Promise","parsedOptions","parseWebCliOptions","tools","WebMidsceneTools","WebCdpMidsceneTools","WebPuppeteerMidsceneTools","runToolsCLI","__VERSION__","createReportCliCommands","e","reportCLIError"],"mappings":";;;;;;;;;AAWA,MAAMA,UAAUC,KAAKC,QAAQ,GAAG,IAAI;AACpC,IAAIC,WAAWH,UACbI,OAAO,MAAM,CAAC;IAAE,MAAMJ;AAAQ;AAKhCK,QAAQ,OAAO,GACZ,IAAI,CAAC;IACJ,MAAMC,gBAAgBC,mBAAmBL,QAAQ,IAAI,CAAC,KAAK,CAAC;IAE5D,IAAIM;IAKFA,QADEF,AAAuB,aAAvBA,cAAc,IAAI,GACZ,IAAIG,qBACHH,AAAuB,UAAvBA,cAAc,IAAI,GACnB,IAAII,oBAAoBJ,cAAc,WAAW,IAEjD,IAAIK,0BAA0BL,cAAc,QAAQ;IAG9D,OAAOM,YAAYJ,OAAO,gBAAgB;QACxC,aAAa;QACb,MAAMF,cAAc,IAAI;QACxB,SAASO;QACT,eAAeC;IACjB;AACF,GACC,KAAK,CAAC,CAACC;IACNb,QAAQ,IAAI,CAACc,eAAeD;AAC9B"}
1
+ {"version":3,"file":"cli.mjs","sources":["../../src/cli.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createReportCliCommands } from '@midscene/core';\nimport { reportCLIError, runToolsCLI } from '@midscene/shared/cli';\nimport dotenv from 'dotenv';\nimport { WebMidsceneTools } from './agent-tools';\nimport { WebCdpMidsceneTools } from './agent-tools-cdp';\nimport { WebPuppeteerMidsceneTools } from './agent-tools-puppeteer';\nimport { parseWebCliOptions } from './cli-options';\n\n// Load .env early so MIDSCENE_CDP_ENDPOINT is available during arg parsing\nconst envFile = join(process.cwd(), '.env');\nif (existsSync(envFile)) {\n dotenv.config({ path: envFile });\n}\n\ndeclare const __VERSION__: string;\n\nPromise.resolve()\n .then(() => {\n const parsedOptions = parseWebCliOptions(process.argv.slice(2));\n\n let tools:\n | WebMidsceneTools\n | WebPuppeteerMidsceneTools\n | WebCdpMidsceneTools;\n if (parsedOptions.mode === 'bridge') {\n tools = new WebMidsceneTools();\n } else if (parsedOptions.mode === 'cdp') {\n tools = new WebCdpMidsceneTools(parsedOptions.cdpEndpoint!);\n } else {\n tools = new WebPuppeteerMidsceneTools(parsedOptions.viewport);\n }\n\n return runToolsCLI(tools, 'midscene-web', {\n stripPrefix: 'web_',\n argv: parsedOptions.argv,\n version: __VERSION__,\n extraCommands: createReportCliCommands(),\n });\n })\n .catch((e) => {\n process.exit(reportCLIError(e));\n });\n"],"names":["envFile","join","process","existsSync","dotenv","Promise","parsedOptions","parseWebCliOptions","tools","WebMidsceneTools","WebCdpMidsceneTools","WebPuppeteerMidsceneTools","runToolsCLI","__VERSION__","createReportCliCommands","e","reportCLIError"],"mappings":";;;;;;;;;AAWA,MAAMA,UAAUC,KAAKC,QAAQ,GAAG,IAAI;AACpC,IAAIC,WAAWH,UACbI,OAAO,MAAM,CAAC;IAAE,MAAMJ;AAAQ;AAKhCK,QAAQ,OAAO,GACZ,IAAI,CAAC;IACJ,MAAMC,gBAAgBC,mBAAmBL,QAAQ,IAAI,CAAC,KAAK,CAAC;IAE5D,IAAIM;IAKFA,QADEF,AAAuB,aAAvBA,cAAc,IAAI,GACZ,IAAIG,qBACHH,AAAuB,UAAvBA,cAAc,IAAI,GACnB,IAAII,oBAAoBJ,cAAc,WAAW,IAEjD,IAAIK,0BAA0BL,cAAc,QAAQ;IAG9D,OAAOM,YAAYJ,OAAO,gBAAgB;QACxC,aAAa;QACb,MAAMF,cAAc,IAAI;QACxB,SAASO;QACT,eAAeC;IACjB;AACF,GACC,KAAK,CAAC,CAACC;IACNb,QAAQ,IAAI,CAACc,eAAeD;AAC9B"}
package/dist/es/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { PlaywrightAgent, PlaywrightAiFixture } from "./playwright/index.mjs";
2
2
  import { Agent } from "@midscene/core/agent";
3
3
  import { PuppeteerAgent } from "./puppeteer/index.mjs";
4
4
  import { StaticPage, StaticPageAgent } from "./static/index.mjs";
5
- import { WebMidsceneTools } from "./mcp-tools.mjs";
5
+ import { WebMidsceneTools } from "./agent-tools.mjs";
6
6
  import { webPlaygroundPlatform } from "./platform.mjs";
7
- import { WebCdpMidsceneTools } from "./mcp-tools-cdp.mjs";
7
+ import { WebCdpMidsceneTools } from "./agent-tools-cdp.mjs";
8
8
  export { Agent as PageAgent, PlaywrightAgent, PlaywrightAiFixture, PuppeteerAgent, StaticPage, StaticPageAgent, WebCdpMidsceneTools, WebMidsceneTools, webPlaygroundPlatform };
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import node_path from "node:path";
2
3
  import { getDebug } from "@midscene/shared/logger";
3
4
  import { assert } from "@midscene/shared/utils";
4
5
  import { defaultViewportHeight, defaultViewportWidth, resolveWebViewportSize } from "../common/viewport.mjs";
@@ -31,6 +32,13 @@ function validateChromeArgs(args, baseArgs) {
31
32
  if (dangerousArgs.length > 0) console.warn(`Warning: Dangerous Chrome arguments detected: ${dangerousArgs.join(', ')}.\nThese arguments may reduce browser security. Use only in controlled testing environments.`);
32
33
  }
33
34
  const launcherDebug = getDebug('puppeteer:launcher');
35
+ function buildDownloadBehavior(downloadPath) {
36
+ if (!downloadPath) return;
37
+ return {
38
+ policy: 'allow',
39
+ downloadPath: node_path.resolve(downloadPath)
40
+ };
41
+ }
34
42
  function buildChromeArgs(options) {
35
43
  const isWindows = 'win32' === process.platform;
36
44
  const sandboxArgs = isWindows ? [] : [
@@ -91,6 +99,7 @@ async function launchPuppeteerPage(target, preference, browser, existingPage) {
91
99
  } : void 0,
92
100
  chromeArgs: target.chromeArgs
93
101
  });
102
+ const downloadBehavior = buildDownloadBehavior(target.downloadPath);
94
103
  launcherDebug('launching browser with viewport, headed', headed, 'viewport', viewportConfig, 'args', args, 'preference', preference);
95
104
  let page;
96
105
  let browserInstance = browser;
@@ -103,6 +112,7 @@ async function launchPuppeteerPage(target, preference, browser, existingPage) {
103
112
  browserInstance = await puppeteer.launch({
104
113
  headless: !preference?.headed,
105
114
  defaultViewport: defaultViewportConfig,
115
+ downloadBehavior,
106
116
  args,
107
117
  acceptInsecureCerts: target.acceptInsecureCerts,
108
118
  ignoreDefaultArgs: preference?.ignoreDefaultArgs
@@ -176,6 +186,6 @@ async function puppeteerAgentForTarget(target, preference, browser, existingPage
176
186
  freeFn
177
187
  };
178
188
  }
179
- export { buildChromeArgs, defaultUA, defaultViewportHeight, defaultViewportScale, defaultViewportWidth, defaultWaitForNetworkIdleTimeout, launchPuppeteerPage, puppeteerAgentForTarget, resolveAiActionContext };
189
+ export { buildChromeArgs, buildDownloadBehavior, defaultUA, defaultViewportHeight, defaultViewportScale, defaultViewportWidth, defaultWaitForNetworkIdleTimeout, launchPuppeteerPage, puppeteerAgentForTarget, resolveAiActionContext };
180
190
 
181
191
  //# sourceMappingURL=agent-launcher.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"puppeteer/agent-launcher.mjs","sources":["../../../src/puppeteer/agent-launcher.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { getDebug } from '@midscene/shared/logger';\nimport { assert } from '@midscene/shared/utils';\n\nimport {\n defaultViewportHeight,\n defaultViewportWidth,\n resolveWebViewportSize,\n} from '@/common/viewport';\nimport { PuppeteerAgent } from '@/puppeteer/index';\nimport type { AgentOpt, Cache, MidsceneYamlScriptWebEnv } from '@midscene/core';\nimport { DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT } from '@midscene/shared/constants';\nimport puppeteer, { type Browser, type Page } from 'puppeteer';\n\nexport { defaultViewportWidth, defaultViewportHeight } from '@/common/viewport';\n\nexport const defaultUA =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36';\n// Setting deviceScaleFactor value to `0` means reset this value to the system default in Puppeteer.\nexport const defaultViewportScale = 0;\nexport const defaultWaitForNetworkIdleTimeout =\n DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT;\n\nexport function resolveAiActionContext(\n target: MidsceneYamlScriptWebEnv,\n preference?: Partial<Pick<AgentOpt, 'aiActionContext' | 'aiActContext'>>,\n): AgentOpt['aiActionContext'] | undefined {\n // Prefer agent-level preference if provided; otherwise fall back to target-level context.\n // Priority: preference.aiActContext > preference.aiActionContext (deprecated) > target.aiActionContext\n const data =\n preference?.aiActContext ??\n preference?.aiActionContext ??\n target.aiActionContext;\n return data;\n}\n\n/**\n * Chrome arguments that may reduce browser security.\n * These should only be used in controlled testing environments.\n *\n * Security implications:\n * - `--no-sandbox`: Disables Chrome's sandbox security model\n * - `--disable-setuid-sandbox`: Disables setuid sandbox on Linux\n * - `--disable-web-security`: Allows cross-origin requests without CORS\n * - `--ignore-certificate-errors`: Ignores SSL/TLS certificate errors\n * - `--disable-features=IsolateOrigins`: Disables origin isolation\n * - `--disable-site-isolation-trials`: Disables site isolation\n * - `--allow-running-insecure-content`: Allows mixed HTTP/HTTPS content\n */\nconst DANGEROUS_ARGS = [\n '--no-sandbox',\n '--disable-setuid-sandbox',\n '--disable-web-security',\n '--ignore-certificate-errors',\n '--disable-features=IsolateOrigins',\n '--disable-site-isolation-trials',\n '--allow-running-insecure-content',\n] as const;\n\n/**\n * Validates Chrome launch arguments for security concerns.\n * Emits a warning if dangerous arguments are detected.\n *\n * This function filters out arguments that are already present in baseArgs\n * to avoid warning about platform-specific defaults (e.g., --no-sandbox on non-Windows).\n *\n * @param args - Chrome launch arguments to validate\n * @param baseArgs - Base Chrome arguments already configured\n *\n * @example\n * ```typescript\n * // Will show warning for --disable-web-security\n * validateChromeArgs(['--disable-web-security', '--headless'], ['--no-sandbox']);\n *\n * // Will NOT show warning for --no-sandbox (already in baseArgs)\n * validateChromeArgs(['--no-sandbox'], ['--no-sandbox', '--headless']);\n * ```\n */\nfunction validateChromeArgs(args: string[], baseArgs: string[]): void {\n // Filter out arguments that are already in baseArgs\n const newArgs = args.filter(\n (arg) =>\n !baseArgs.some((baseArg) => {\n // Check if arg starts with the same flag as baseArg (before '=' if present)\n const argFlag = arg.split('=')[0];\n const baseFlag = baseArg.split('=')[0];\n return argFlag === baseFlag;\n }),\n );\n\n const dangerousArgs = newArgs.filter((arg) =>\n DANGEROUS_ARGS.some((dangerous) => arg.startsWith(dangerous)),\n );\n\n if (dangerousArgs.length > 0) {\n console.warn(\n `Warning: Dangerous Chrome arguments detected: ${dangerousArgs.join(', ')}.\\nThese arguments may reduce browser security. Use only in controlled testing environments.`,\n );\n }\n}\n\ninterface FreeFn {\n name: string;\n fn: () => void;\n}\n\nconst launcherDebug = getDebug('puppeteer:launcher');\n\nexport interface BuildChromeArgsOptions {\n userAgent?: string;\n windowSize?: { width: number; height: number };\n chromeArgs?: string[];\n}\n\n/**\n * Builds Chrome launch arguments with sensible defaults.\n *\n * Platform-specific behavior:\n * - On non-Windows systems, automatically adds --no-sandbox and --disable-setuid-sandbox\n * for compatibility with containerized/CI environments\n *\n * @param options - Configuration options for Chrome arguments\n * @returns Array of Chrome launch arguments\n *\n * @example\n * ```typescript\n * // Basic usage\n * const args = buildChromeArgs();\n *\n * // With custom arguments\n * const args = buildChromeArgs({\n * chromeArgs: ['--disable-gpu', '--disable-dev-shm-usage'],\n * userAgent: 'CustomUA/1.0',\n * windowSize: { width: 1920, height: 1080 },\n * });\n * ```\n */\nexport function buildChromeArgs(options?: BuildChromeArgsOptions): string[] {\n const isWindows = process.platform === 'win32';\n\n const sandboxArgs = isWindows\n ? []\n : ['--no-sandbox', '--disable-setuid-sandbox'];\n const featureArgs = [\n '--disable-features=HttpsFirstBalancedModeAutoEnable',\n '--disable-features=PasswordLeakDetection',\n '--disable-save-password-bubble',\n ];\n const userAgentArg = options?.userAgent\n ? [`--user-agent=\"${options.userAgent}\"`]\n : [];\n const windowSizeArg = options?.windowSize\n ? [`--window-size=${options.windowSize.width},${options.windowSize.height}`]\n : [];\n\n const baseArgs = [\n ...sandboxArgs,\n ...featureArgs,\n ...userAgentArg,\n ...windowSizeArg,\n ];\n\n if (options?.chromeArgs?.length) {\n validateChromeArgs(options.chromeArgs, baseArgs);\n return [...baseArgs, ...options.chromeArgs];\n }\n\n return baseArgs;\n}\n\nexport async function launchPuppeteerPage(\n target: MidsceneYamlScriptWebEnv,\n preference?: {\n headed?: boolean;\n keepWindow?: boolean;\n ignoreDefaultArgs?: boolean | string[];\n },\n browser?: Browser,\n existingPage?: Page,\n) {\n assert(target.url, 'url is required');\n const freeFn: FreeFn[] = [];\n\n // prepare the environment\n const ua = target.userAgent || defaultUA;\n const { width, height } = resolveWebViewportSize(target);\n let dpr = defaultViewportScale;\n if (\n target.deviceScaleFactor !== undefined &&\n target.deviceScaleFactor !== null\n ) {\n assert(\n typeof target.deviceScaleFactor === 'number',\n 'deviceScaleFactor must be a number',\n );\n dpr = target.deviceScaleFactor;\n assert(dpr > 0, `deviceScaleFactor must be > 0, but got ${dpr}`);\n }\n const viewportConfig = {\n width,\n height,\n deviceScaleFactor: dpr,\n };\n\n const headed = preference?.headed || preference?.keepWindow;\n const defaultViewportConfig = headed ? null : viewportConfig;\n\n // launch the browser\n if (headed && process.env.CI === '1') {\n console.warn(\n 'you are probably running headed mode in CI, this will usually fail.',\n );\n }\n\n // Build Chrome arguments using the shared helper\n // Only pass windowSize in headed mode; in headless mode, defaultViewport takes precedence\n // Add 100px to height to account for browser UI (address bar, tabs, etc.)\n const browserUIHeight = 100;\n const args = buildChromeArgs({\n userAgent: ua,\n windowSize: headed\n ? { width, height: height + browserUIHeight }\n : undefined,\n chromeArgs: target.chromeArgs,\n });\n\n launcherDebug(\n 'launching browser with viewport, headed',\n headed,\n 'viewport',\n viewportConfig,\n 'args',\n args,\n 'preference',\n preference,\n );\n // If an existing page is provided, reuse it instead of creating a new one\n // This allows sharing localStorage and sessionStorage between YAML files\n let page: Page;\n let browserInstance = browser;\n\n if (existingPage) {\n // Reuse the existing page - this preserves localStorage and sessionStorage\n page = existingPage;\n launcherDebug('reusing existing page for shared browser context');\n\n // Get the browser instance from the existing page\n if (!browserInstance) {\n browserInstance = page.browser();\n }\n } else {\n // Create a new browser and page\n if (!browserInstance) {\n browserInstance = await puppeteer.launch({\n headless: !preference?.headed,\n defaultViewport: defaultViewportConfig,\n args,\n acceptInsecureCerts: target.acceptInsecureCerts,\n ignoreDefaultArgs: preference?.ignoreDefaultArgs,\n });\n freeFn.push({\n name: 'puppeteer_browser',\n fn: () => {\n if (!preference?.keepWindow) {\n if (process.platform === 'win32') {\n setTimeout(() => {\n browserInstance?.close();\n }, 800);\n } else {\n browserInstance?.close();\n }\n }\n },\n });\n }\n page = await browserInstance.newPage();\n }\n\n if (target.cookie) {\n const cookieFileContent = readFileSync(target.cookie, 'utf-8');\n await browserInstance.setCookie(...JSON.parse(cookieFileContent));\n }\n\n if (ua) {\n await page.setUserAgent(ua);\n }\n\n if (target.extraHTTPHeaders) {\n // YAML may parse unquoted values into booleans/numbers (e.g. `yes` -> true),\n // but Puppeteer requires string header values, so normalize them here.\n const normalizedHeaders = Object.fromEntries(\n Object.entries(target.extraHTTPHeaders).map(([key, value]) => [\n key,\n String(value),\n ]),\n );\n await page.setExtraHTTPHeaders(normalizedHeaders);\n }\n\n if (viewportConfig) {\n await page.setViewport(viewportConfig);\n }\n\n const waitForNetworkIdleTimeout =\n typeof target.waitForNetworkIdle?.timeout === 'number'\n ? target.waitForNetworkIdle.timeout\n : defaultWaitForNetworkIdleTimeout;\n\n try {\n launcherDebug('goto', target.url);\n await page.goto(target.url);\n if (waitForNetworkIdleTimeout > 0) {\n launcherDebug('waitForNetworkIdle', waitForNetworkIdleTimeout);\n await page.waitForNetworkIdle({\n timeout: waitForNetworkIdleTimeout,\n });\n }\n } catch (e) {\n if (\n typeof target.waitForNetworkIdle?.continueOnNetworkIdleError ===\n 'boolean' &&\n !target.waitForNetworkIdle?.continueOnNetworkIdleError\n ) {\n const newError = new Error(`failed to wait for network idle: ${e}`, {\n cause: e,\n });\n throw newError;\n }\n const newMessage = `failed to wait for network idle after ${waitForNetworkIdleTimeout}ms, but the script will continue.`;\n console.warn(newMessage);\n }\n\n return { page, freeFn };\n}\n\nexport async function puppeteerAgentForTarget(\n target: MidsceneYamlScriptWebEnv,\n preference?: {\n headed?: boolean;\n keepWindow?: boolean;\n } & Partial<\n Pick<\n AgentOpt,\n | 'groupName'\n | 'groupDescription'\n | 'generateReport'\n | 'persistExecutionDump'\n | 'autoPrintReportMsg'\n | 'reportFileName'\n | 'replanningCycleLimit'\n | 'cache'\n | 'aiActionContext'\n >\n >,\n browser?: Browser,\n existingPage?: Page,\n) {\n const { page, freeFn } = await launchPuppeteerPage(\n target,\n preference,\n browser,\n existingPage,\n );\n const aiActContext = resolveAiActionContext(target, preference);\n\n const { aiActionContext, ...preferenceToUse } = preference ?? {};\n\n // prepare Midscene agent\n const agent = new PuppeteerAgent(page, {\n ...preferenceToUse,\n aiActContext,\n waitForNetworkIdleTimeout:\n typeof target.waitForNetworkIdle?.timeout === 'number'\n ? target.waitForNetworkIdle.timeout\n : undefined,\n forceSameTabNavigation:\n typeof target.forceSameTabNavigation !== 'undefined'\n ? target.forceSameTabNavigation\n : true, // true for default in yaml script\n });\n\n freeFn.push({\n name: 'midscene_puppeteer_agent',\n fn: () => agent.destroy(),\n });\n\n return { agent, freeFn };\n}\n"],"names":["defaultUA","defaultViewportScale","defaultWaitForNetworkIdleTimeout","DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT","resolveAiActionContext","target","preference","data","DANGEROUS_ARGS","validateChromeArgs","args","baseArgs","newArgs","arg","baseArg","argFlag","baseFlag","dangerousArgs","dangerous","console","launcherDebug","getDebug","buildChromeArgs","options","isWindows","process","sandboxArgs","featureArgs","userAgentArg","windowSizeArg","launchPuppeteerPage","browser","existingPage","assert","freeFn","ua","width","height","resolveWebViewportSize","dpr","undefined","viewportConfig","headed","defaultViewportConfig","browserUIHeight","page","browserInstance","puppeteer","setTimeout","cookieFileContent","readFileSync","JSON","normalizedHeaders","Object","key","value","String","waitForNetworkIdleTimeout","e","newError","Error","newMessage","puppeteerAgentForTarget","aiActContext","aiActionContext","preferenceToUse","agent","PuppeteerAgent"],"mappings":"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"}
1
+ {"version":3,"file":"puppeteer/agent-launcher.mjs","sources":["../../../src/puppeteer/agent-launcher.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { getDebug } from '@midscene/shared/logger';\nimport { assert } from '@midscene/shared/utils';\n\nimport {\n defaultViewportHeight,\n defaultViewportWidth,\n resolveWebViewportSize,\n} from '@/common/viewport';\nimport { PuppeteerAgent } from '@/puppeteer/index';\nimport type { AgentOpt, Cache, MidsceneYamlScriptWebEnv } from '@midscene/core';\nimport { DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT } from '@midscene/shared/constants';\nimport puppeteer, {\n type Browser,\n type DownloadBehavior,\n type Page,\n} from 'puppeteer';\n\nexport { defaultViewportWidth, defaultViewportHeight } from '@/common/viewport';\n\nexport const defaultUA =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36';\n// Setting deviceScaleFactor value to `0` means reset this value to the system default in Puppeteer.\nexport const defaultViewportScale = 0;\nexport const defaultWaitForNetworkIdleTimeout =\n DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT;\n\nexport function resolveAiActionContext(\n target: MidsceneYamlScriptWebEnv,\n preference?: Partial<Pick<AgentOpt, 'aiActionContext' | 'aiActContext'>>,\n): AgentOpt['aiActionContext'] | undefined {\n // Prefer agent-level preference if provided; otherwise fall back to target-level context.\n // Priority: preference.aiActContext > preference.aiActionContext (deprecated) > target.aiActionContext\n const data =\n preference?.aiActContext ??\n preference?.aiActionContext ??\n target.aiActionContext;\n return data;\n}\n\n/**\n * Chrome arguments that may reduce browser security.\n * These should only be used in controlled testing environments.\n *\n * Security implications:\n * - `--no-sandbox`: Disables Chrome's sandbox security model\n * - `--disable-setuid-sandbox`: Disables setuid sandbox on Linux\n * - `--disable-web-security`: Allows cross-origin requests without CORS\n * - `--ignore-certificate-errors`: Ignores SSL/TLS certificate errors\n * - `--disable-features=IsolateOrigins`: Disables origin isolation\n * - `--disable-site-isolation-trials`: Disables site isolation\n * - `--allow-running-insecure-content`: Allows mixed HTTP/HTTPS content\n */\nconst DANGEROUS_ARGS = [\n '--no-sandbox',\n '--disable-setuid-sandbox',\n '--disable-web-security',\n '--ignore-certificate-errors',\n '--disable-features=IsolateOrigins',\n '--disable-site-isolation-trials',\n '--allow-running-insecure-content',\n] as const;\n\n/**\n * Validates Chrome launch arguments for security concerns.\n * Emits a warning if dangerous arguments are detected.\n *\n * This function filters out arguments that are already present in baseArgs\n * to avoid warning about platform-specific defaults (e.g., --no-sandbox on non-Windows).\n *\n * @param args - Chrome launch arguments to validate\n * @param baseArgs - Base Chrome arguments already configured\n *\n * @example\n * ```typescript\n * // Will show warning for --disable-web-security\n * validateChromeArgs(['--disable-web-security', '--headless'], ['--no-sandbox']);\n *\n * // Will NOT show warning for --no-sandbox (already in baseArgs)\n * validateChromeArgs(['--no-sandbox'], ['--no-sandbox', '--headless']);\n * ```\n */\nfunction validateChromeArgs(args: string[], baseArgs: string[]): void {\n // Filter out arguments that are already in baseArgs\n const newArgs = args.filter(\n (arg) =>\n !baseArgs.some((baseArg) => {\n // Check if arg starts with the same flag as baseArg (before '=' if present)\n const argFlag = arg.split('=')[0];\n const baseFlag = baseArg.split('=')[0];\n return argFlag === baseFlag;\n }),\n );\n\n const dangerousArgs = newArgs.filter((arg) =>\n DANGEROUS_ARGS.some((dangerous) => arg.startsWith(dangerous)),\n );\n\n if (dangerousArgs.length > 0) {\n console.warn(\n `Warning: Dangerous Chrome arguments detected: ${dangerousArgs.join(', ')}.\\nThese arguments may reduce browser security. Use only in controlled testing environments.`,\n );\n }\n}\n\ninterface FreeFn {\n name: string;\n fn: () => void;\n}\n\nconst launcherDebug = getDebug('puppeteer:launcher');\n\nexport function buildDownloadBehavior(\n downloadPath: string | undefined,\n): DownloadBehavior | undefined {\n if (!downloadPath) {\n return undefined;\n }\n\n return {\n policy: 'allow',\n downloadPath: path.resolve(downloadPath),\n };\n}\n\nexport interface BuildChromeArgsOptions {\n userAgent?: string;\n windowSize?: { width: number; height: number };\n chromeArgs?: string[];\n}\n\n/**\n * Builds Chrome launch arguments with sensible defaults.\n *\n * Platform-specific behavior:\n * - On non-Windows systems, automatically adds --no-sandbox and --disable-setuid-sandbox\n * for compatibility with containerized/CI environments\n *\n * @param options - Configuration options for Chrome arguments\n * @returns Array of Chrome launch arguments\n *\n * @example\n * ```typescript\n * // Basic usage\n * const args = buildChromeArgs();\n *\n * // With custom arguments\n * const args = buildChromeArgs({\n * chromeArgs: ['--disable-gpu', '--disable-dev-shm-usage'],\n * userAgent: 'CustomUA/1.0',\n * windowSize: { width: 1920, height: 1080 },\n * });\n * ```\n */\nexport function buildChromeArgs(options?: BuildChromeArgsOptions): string[] {\n const isWindows = process.platform === 'win32';\n\n const sandboxArgs = isWindows\n ? []\n : ['--no-sandbox', '--disable-setuid-sandbox'];\n const featureArgs = [\n '--disable-features=HttpsFirstBalancedModeAutoEnable',\n '--disable-features=PasswordLeakDetection',\n '--disable-save-password-bubble',\n ];\n const userAgentArg = options?.userAgent\n ? [`--user-agent=\"${options.userAgent}\"`]\n : [];\n const windowSizeArg = options?.windowSize\n ? [`--window-size=${options.windowSize.width},${options.windowSize.height}`]\n : [];\n\n const baseArgs = [\n ...sandboxArgs,\n ...featureArgs,\n ...userAgentArg,\n ...windowSizeArg,\n ];\n\n if (options?.chromeArgs?.length) {\n validateChromeArgs(options.chromeArgs, baseArgs);\n return [...baseArgs, ...options.chromeArgs];\n }\n\n return baseArgs;\n}\n\nexport async function launchPuppeteerPage(\n target: MidsceneYamlScriptWebEnv,\n preference?: {\n headed?: boolean;\n keepWindow?: boolean;\n ignoreDefaultArgs?: boolean | string[];\n },\n browser?: Browser,\n existingPage?: Page,\n) {\n assert(target.url, 'url is required');\n const freeFn: FreeFn[] = [];\n\n // prepare the environment\n const ua = target.userAgent || defaultUA;\n const { width, height } = resolveWebViewportSize(target);\n let dpr = defaultViewportScale;\n if (\n target.deviceScaleFactor !== undefined &&\n target.deviceScaleFactor !== null\n ) {\n assert(\n typeof target.deviceScaleFactor === 'number',\n 'deviceScaleFactor must be a number',\n );\n dpr = target.deviceScaleFactor;\n assert(dpr > 0, `deviceScaleFactor must be > 0, but got ${dpr}`);\n }\n const viewportConfig = {\n width,\n height,\n deviceScaleFactor: dpr,\n };\n\n const headed = preference?.headed || preference?.keepWindow;\n const defaultViewportConfig = headed ? null : viewportConfig;\n\n // launch the browser\n if (headed && process.env.CI === '1') {\n console.warn(\n 'you are probably running headed mode in CI, this will usually fail.',\n );\n }\n\n // Build Chrome arguments using the shared helper\n // Only pass windowSize in headed mode; in headless mode, defaultViewport takes precedence\n // Add 100px to height to account for browser UI (address bar, tabs, etc.)\n const browserUIHeight = 100;\n const args = buildChromeArgs({\n userAgent: ua,\n windowSize: headed\n ? { width, height: height + browserUIHeight }\n : undefined,\n chromeArgs: target.chromeArgs,\n });\n const downloadBehavior = buildDownloadBehavior(target.downloadPath);\n\n launcherDebug(\n 'launching browser with viewport, headed',\n headed,\n 'viewport',\n viewportConfig,\n 'args',\n args,\n 'preference',\n preference,\n );\n // If an existing page is provided, reuse it instead of creating a new one\n // This allows sharing localStorage and sessionStorage between YAML files\n let page: Page;\n let browserInstance = browser;\n\n if (existingPage) {\n // Reuse the existing page - this preserves localStorage and sessionStorage\n page = existingPage;\n launcherDebug('reusing existing page for shared browser context');\n\n // Get the browser instance from the existing page\n if (!browserInstance) {\n browserInstance = page.browser();\n }\n } else {\n // Create a new browser and page\n if (!browserInstance) {\n browserInstance = await puppeteer.launch({\n headless: !preference?.headed,\n defaultViewport: defaultViewportConfig,\n downloadBehavior,\n args,\n acceptInsecureCerts: target.acceptInsecureCerts,\n ignoreDefaultArgs: preference?.ignoreDefaultArgs,\n });\n freeFn.push({\n name: 'puppeteer_browser',\n fn: () => {\n if (!preference?.keepWindow) {\n if (process.platform === 'win32') {\n setTimeout(() => {\n browserInstance?.close();\n }, 800);\n } else {\n browserInstance?.close();\n }\n }\n },\n });\n }\n page = await browserInstance.newPage();\n }\n\n if (target.cookie) {\n const cookieFileContent = readFileSync(target.cookie, 'utf-8');\n await browserInstance.setCookie(...JSON.parse(cookieFileContent));\n }\n\n if (ua) {\n await page.setUserAgent(ua);\n }\n\n if (target.extraHTTPHeaders) {\n // YAML may parse unquoted values into booleans/numbers (e.g. `yes` -> true),\n // but Puppeteer requires string header values, so normalize them here.\n const normalizedHeaders = Object.fromEntries(\n Object.entries(target.extraHTTPHeaders).map(([key, value]) => [\n key,\n String(value),\n ]),\n );\n await page.setExtraHTTPHeaders(normalizedHeaders);\n }\n\n if (viewportConfig) {\n await page.setViewport(viewportConfig);\n }\n\n const waitForNetworkIdleTimeout =\n typeof target.waitForNetworkIdle?.timeout === 'number'\n ? target.waitForNetworkIdle.timeout\n : defaultWaitForNetworkIdleTimeout;\n\n try {\n launcherDebug('goto', target.url);\n await page.goto(target.url);\n if (waitForNetworkIdleTimeout > 0) {\n launcherDebug('waitForNetworkIdle', waitForNetworkIdleTimeout);\n await page.waitForNetworkIdle({\n timeout: waitForNetworkIdleTimeout,\n });\n }\n } catch (e) {\n if (\n typeof target.waitForNetworkIdle?.continueOnNetworkIdleError ===\n 'boolean' &&\n !target.waitForNetworkIdle?.continueOnNetworkIdleError\n ) {\n const newError = new Error(`failed to wait for network idle: ${e}`, {\n cause: e,\n });\n throw newError;\n }\n const newMessage = `failed to wait for network idle after ${waitForNetworkIdleTimeout}ms, but the script will continue.`;\n console.warn(newMessage);\n }\n\n return { page, freeFn };\n}\n\nexport async function puppeteerAgentForTarget(\n target: MidsceneYamlScriptWebEnv,\n preference?: {\n headed?: boolean;\n keepWindow?: boolean;\n } & Partial<\n Pick<\n AgentOpt,\n | 'groupName'\n | 'groupDescription'\n | 'generateReport'\n | 'persistExecutionDump'\n | 'autoPrintReportMsg'\n | 'reportFileName'\n | 'replanningCycleLimit'\n | 'cache'\n | 'aiActionContext'\n >\n >,\n browser?: Browser,\n existingPage?: Page,\n) {\n const { page, freeFn } = await launchPuppeteerPage(\n target,\n preference,\n browser,\n existingPage,\n );\n const aiActContext = resolveAiActionContext(target, preference);\n\n const { aiActionContext, ...preferenceToUse } = preference ?? {};\n\n // prepare Midscene agent\n const agent = new PuppeteerAgent(page, {\n ...preferenceToUse,\n aiActContext,\n waitForNetworkIdleTimeout:\n typeof target.waitForNetworkIdle?.timeout === 'number'\n ? target.waitForNetworkIdle.timeout\n : undefined,\n forceSameTabNavigation:\n typeof target.forceSameTabNavigation !== 'undefined'\n ? target.forceSameTabNavigation\n : true, // true for default in yaml script\n });\n\n freeFn.push({\n name: 'midscene_puppeteer_agent',\n fn: () => agent.destroy(),\n });\n\n return { agent, freeFn };\n}\n"],"names":["defaultUA","defaultViewportScale","defaultWaitForNetworkIdleTimeout","DEFAULT_WAIT_FOR_NETWORK_IDLE_TIMEOUT","resolveAiActionContext","target","preference","data","DANGEROUS_ARGS","validateChromeArgs","args","baseArgs","newArgs","arg","baseArg","argFlag","baseFlag","dangerousArgs","dangerous","console","launcherDebug","getDebug","buildDownloadBehavior","downloadPath","path","buildChromeArgs","options","isWindows","process","sandboxArgs","featureArgs","userAgentArg","windowSizeArg","launchPuppeteerPage","browser","existingPage","assert","freeFn","ua","width","height","resolveWebViewportSize","dpr","undefined","viewportConfig","headed","defaultViewportConfig","browserUIHeight","downloadBehavior","page","browserInstance","puppeteer","setTimeout","cookieFileContent","readFileSync","JSON","normalizedHeaders","Object","key","value","String","waitForNetworkIdleTimeout","e","newError","Error","newMessage","puppeteerAgentForTarget","aiActContext","aiActionContext","preferenceToUse","agent","PuppeteerAgent"],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ adaptWebAgentInitArgs: ()=>adaptWebAgentInitArgs,
28
+ webAgentInitArgShape: ()=>webAgentInitArgShape
29
+ });
30
+ const core_namespaceObject = require("@midscene/core");
31
+ const agent_behavior_init_args_namespaceObject = require("@midscene/shared/agent-tools/agent-behavior-init-args");
32
+ const webAgentInitArgShape = {
33
+ url: core_namespaceObject.z.string().url().optional().describe('URL to open in new tab (omit to use current page)'),
34
+ ...agent_behavior_init_args_namespaceObject.agentBehaviorInitArgShape
35
+ };
36
+ function adaptWebAgentInitArgs(extracted) {
37
+ if (!extracted) return;
38
+ const initArgs = {
39
+ ...'string' == typeof extracted.url ? {
40
+ url: extracted.url
41
+ } : {},
42
+ ...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(extracted) ?? {}
43
+ };
44
+ return Object.keys(initArgs).length > 0 ? initArgs : void 0;
45
+ }
46
+ exports.adaptWebAgentInitArgs = __webpack_exports__.adaptWebAgentInitArgs;
47
+ exports.webAgentInitArgShape = __webpack_exports__.webAgentInitArgShape;
48
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
49
+ "adaptWebAgentInitArgs",
50
+ "webAgentInitArgShape"
51
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
52
+ Object.defineProperty(exports, '__esModule', {
53
+ value: true
54
+ });
55
+
56
+ //# sourceMappingURL=agent-init-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-init-args.js","sources":["webpack/runtime/define_property_getters","webpack/runtime/has_own_property","webpack/runtime/make_namespace_object","../../src/agent-init-args.ts"],"sourcesContent":["__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n }\n }\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import { z } from '@midscene/core';\nimport {\n type AgentBehaviorInitArgs,\n agentBehaviorInitArgShape,\n extractAgentBehaviorInitArgs,\n} from '@midscene/shared/agent-tools/agent-behavior-init-args';\n\nexport type WebAgentInitArgs = AgentBehaviorInitArgs & {\n url?: string;\n};\n\nexport const webAgentInitArgShape = {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n ...agentBehaviorInitArgShape,\n};\n\nexport function adaptWebAgentInitArgs(\n extracted: Record<string, unknown> | undefined,\n): WebAgentInitArgs | undefined {\n if (!extracted) {\n return undefined;\n }\n\n const initArgs: WebAgentInitArgs = {\n ...(typeof extracted.url === 'string' ? { url: extracted.url } : {}),\n ...(extractAgentBehaviorInitArgs(extracted as AgentBehaviorInitArgs) ?? {}),\n };\n\n return Object.keys(initArgs).length > 0 ? initArgs : undefined;\n}\n"],"names":["__webpack_require__","definition","key","Object","obj","prop","Symbol","webAgentInitArgShape","z","agentBehaviorInitArgShape","adaptWebAgentInitArgs","extracted","initArgs","extractAgentBehaviorInitArgs","undefined"],"mappings":";;;IAAAA,oBAAoB,CAAC,GAAG,CAAC,UAASC;QACjC,IAAI,IAAIC,OAAOD,WACR,IAAGD,oBAAoB,CAAC,CAACC,YAAYC,QAAQ,CAACF,oBAAoB,CAAC,CAAC,UAASE,MACzEC,OAAO,cAAc,CAAC,UAASD,KAAK;YAAE,YAAY;YAAM,KAAKD,UAAU,CAACC,IAAI;QAAC;IAGzF;;;ICNAF,oBAAoB,CAAC,GAAG,CAACI,KAAKC,OAAUF,OAAO,SAAS,CAAC,cAAc,CAAC,IAAI,CAACC,KAAKC;;;ICClFL,oBAAoB,CAAC,GAAG,CAAC;QACxB,IAAG,AAAkB,eAAlB,OAAOM,UAA0BA,OAAO,WAAW,EACrDH,OAAO,cAAc,CAAC,UAASG,OAAO,WAAW,EAAE;YAAE,OAAO;QAAS;QAEtEH,OAAO,cAAc,CAAC,UAAS,cAAc;YAAE,OAAO;QAAK;IAC5D;;;;;;;;;;ACKO,MAAMI,uBAAuB;IAClC,KAAKC,qBAAAA,CAAAA,CAAAA,MACI,GACN,GAAG,GACH,QAAQ,GACR,QAAQ,CAAC;IACZ,GAAGC,yCAAAA,yBAAyB;AAC9B;AAEO,SAASC,sBACdC,SAA8C;IAE9C,IAAI,CAACA,WACH;IAGF,MAAMC,WAA6B;QACjC,GAAI,AAAyB,YAAzB,OAAOD,UAAU,GAAG,GAAgB;YAAE,KAAKA,UAAU,GAAG;QAAC,IAAI,CAAC,CAAC;QACnE,GAAIE,AAAAA,IAAAA,yCAAAA,4BAAAA,AAAAA,EAA6BF,cAAuC,CAAC,CAAC;IAC5E;IAEA,OAAOR,OAAO,IAAI,CAACS,UAAU,MAAM,GAAG,IAAIA,WAAWE;AACvD"}