@godscene/web 1.7.11

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 (129) hide show
  1. package/README.md +7 -0
  2. package/bin/midscene-playground +3 -0
  3. package/bin/midscene-web +2 -0
  4. package/dist/es/bin.mjs +14 -0
  5. package/dist/es/bridge-mode/agent-cli-side.mjs +135 -0
  6. package/dist/es/bridge-mode/browser.mjs +2 -0
  7. package/dist/es/bridge-mode/common.mjs +41 -0
  8. package/dist/es/bridge-mode/index.mjs +4 -0
  9. package/dist/es/bridge-mode/io-client.mjs +99 -0
  10. package/dist/es/bridge-mode/io-server.mjs +218 -0
  11. package/dist/es/bridge-mode/page-browser-side.mjs +119 -0
  12. package/dist/es/cdp-proxy-constants.mjs +7 -0
  13. package/dist/es/cdp-proxy-manager.mjs +217 -0
  14. package/dist/es/cdp-proxy.mjs +151 -0
  15. package/dist/es/cdp-target-store.mjs +26 -0
  16. package/dist/es/chrome-extension/agent.mjs +8 -0
  17. package/dist/es/chrome-extension/cdpInput.mjs +172 -0
  18. package/dist/es/chrome-extension/cdpInput.mjs.LICENSE.txt +5 -0
  19. package/dist/es/chrome-extension/dynamic-scripts.mjs +36 -0
  20. package/dist/es/chrome-extension/index.mjs +5 -0
  21. package/dist/es/chrome-extension/page.mjs +733 -0
  22. package/dist/es/cli-options.mjs +97 -0
  23. package/dist/es/cli.mjs +26 -0
  24. package/dist/es/common/cache-helper.mjs +26 -0
  25. package/dist/es/common/viewport.mjs +36 -0
  26. package/dist/es/index.mjs +8 -0
  27. package/dist/es/mcp-server.mjs +33 -0
  28. package/dist/es/mcp-tools-cdp.mjs +164 -0
  29. package/dist/es/mcp-tools-puppeteer.mjs +246 -0
  30. package/dist/es/mcp-tools.mjs +81 -0
  31. package/dist/es/platform.mjs +37 -0
  32. package/dist/es/playwright/ai-fixture.mjs +364 -0
  33. package/dist/es/playwright/index.mjs +36 -0
  34. package/dist/es/playwright/page.mjs +42 -0
  35. package/dist/es/playwright/reporter/index.mjs +178 -0
  36. package/dist/es/puppeteer/agent-launcher.mjs +172 -0
  37. package/dist/es/puppeteer/base-page.mjs +830 -0
  38. package/dist/es/puppeteer/index.mjs +35 -0
  39. package/dist/es/puppeteer/page.mjs +7 -0
  40. package/dist/es/static/index.mjs +3 -0
  41. package/dist/es/static/static-agent.mjs +10 -0
  42. package/dist/es/static/static-page.mjs +123 -0
  43. package/dist/es/utils.mjs +6 -0
  44. package/dist/es/web-element.mjs +57 -0
  45. package/dist/es/web-page.mjs +272 -0
  46. package/dist/lib/bin.js +20 -0
  47. package/dist/lib/bridge-mode/agent-cli-side.js +172 -0
  48. package/dist/lib/bridge-mode/browser.js +36 -0
  49. package/dist/lib/bridge-mode/common.js +105 -0
  50. package/dist/lib/bridge-mode/index.js +44 -0
  51. package/dist/lib/bridge-mode/io-client.js +133 -0
  52. package/dist/lib/bridge-mode/io-server.js +255 -0
  53. package/dist/lib/bridge-mode/page-browser-side.js +163 -0
  54. package/dist/lib/cdp-proxy-constants.js +50 -0
  55. package/dist/lib/cdp-proxy-manager.js +273 -0
  56. package/dist/lib/cdp-proxy.js +179 -0
  57. package/dist/lib/cdp-target-store.js +66 -0
  58. package/dist/lib/chrome-extension/agent.js +42 -0
  59. package/dist/lib/chrome-extension/cdpInput.js +206 -0
  60. package/dist/lib/chrome-extension/cdpInput.js.LICENSE.txt +5 -0
  61. package/dist/lib/chrome-extension/dynamic-scripts.js +86 -0
  62. package/dist/lib/chrome-extension/index.js +58 -0
  63. package/dist/lib/chrome-extension/page.js +767 -0
  64. package/dist/lib/cli-options.js +131 -0
  65. package/dist/lib/cli.js +54 -0
  66. package/dist/lib/common/cache-helper.js +66 -0
  67. package/dist/lib/common/viewport.js +88 -0
  68. package/dist/lib/index.js +66 -0
  69. package/dist/lib/mcp-server.js +73 -0
  70. package/dist/lib/mcp-tools-cdp.js +208 -0
  71. package/dist/lib/mcp-tools-puppeteer.js +296 -0
  72. package/dist/lib/mcp-tools.js +115 -0
  73. package/dist/lib/platform.js +71 -0
  74. package/dist/lib/playwright/ai-fixture.js +401 -0
  75. package/dist/lib/playwright/index.js +89 -0
  76. package/dist/lib/playwright/page.js +76 -0
  77. package/dist/lib/playwright/reporter/index.js +212 -0
  78. package/dist/lib/puppeteer/agent-launcher.js +240 -0
  79. package/dist/lib/puppeteer/base-page.js +876 -0
  80. package/dist/lib/puppeteer/index.js +85 -0
  81. package/dist/lib/puppeteer/page.js +41 -0
  82. package/dist/lib/static/index.js +50 -0
  83. package/dist/lib/static/static-agent.js +44 -0
  84. package/dist/lib/static/static-page.js +157 -0
  85. package/dist/lib/utils.js +38 -0
  86. package/dist/lib/web-element.js +94 -0
  87. package/dist/lib/web-page.js +322 -0
  88. package/dist/types/bin.d.ts +1 -0
  89. package/dist/types/bridge-mode/agent-cli-side.d.ts +49 -0
  90. package/dist/types/bridge-mode/browser.d.ts +2 -0
  91. package/dist/types/bridge-mode/common.d.ts +74 -0
  92. package/dist/types/bridge-mode/index.d.ts +4 -0
  93. package/dist/types/bridge-mode/io-client.d.ts +10 -0
  94. package/dist/types/bridge-mode/io-server.d.ts +27 -0
  95. package/dist/types/bridge-mode/page-browser-side.d.ts +21 -0
  96. package/dist/types/cdp-proxy-constants.d.ts +4 -0
  97. package/dist/types/cdp-proxy-manager.d.ts +53 -0
  98. package/dist/types/cdp-proxy.d.ts +37 -0
  99. package/dist/types/cdp-target-store.d.ts +26 -0
  100. package/dist/types/chrome-extension/agent.d.ts +4 -0
  101. package/dist/types/chrome-extension/cdpInput.d.ts +52 -0
  102. package/dist/types/chrome-extension/dynamic-scripts.d.ts +3 -0
  103. package/dist/types/chrome-extension/index.d.ts +5 -0
  104. package/dist/types/chrome-extension/page.d.ts +120 -0
  105. package/dist/types/cli-options.d.ts +8 -0
  106. package/dist/types/cli.d.ts +1 -0
  107. package/dist/types/common/cache-helper.d.ts +20 -0
  108. package/dist/types/common/viewport.d.ts +17 -0
  109. package/dist/types/index.d.ts +9 -0
  110. package/dist/types/mcp-server.d.ts +26 -0
  111. package/dist/types/mcp-tools-cdp.d.ts +23 -0
  112. package/dist/types/mcp-tools-puppeteer.d.ts +23 -0
  113. package/dist/types/mcp-tools.d.ts +14 -0
  114. package/dist/types/platform.d.ts +10 -0
  115. package/dist/types/playwright/ai-fixture.d.ts +133 -0
  116. package/dist/types/playwright/index.d.ts +13 -0
  117. package/dist/types/playwright/page.d.ts +11 -0
  118. package/dist/types/playwright/reporter/index.d.ts +28 -0
  119. package/dist/types/puppeteer/agent-launcher.d.ts +59 -0
  120. package/dist/types/puppeteer/base-page.d.ts +123 -0
  121. package/dist/types/puppeteer/index.d.ts +11 -0
  122. package/dist/types/puppeteer/page.d.ts +6 -0
  123. package/dist/types/static/index.d.ts +2 -0
  124. package/dist/types/static/static-agent.d.ts +5 -0
  125. package/dist/types/static/static-page.d.ts +46 -0
  126. package/dist/types/utils.d.ts +6 -0
  127. package/dist/types/web-element.d.ts +48 -0
  128. package/dist/types/web-page.d.ts +69 -0
  129. package/package.json +173 -0
@@ -0,0 +1,97 @@
1
+ import { CLIError } from "@godscene/shared/cli";
2
+ import { defaultPuppeteerWindowViewportSize, resolveViewportSize } from "./common/viewport.mjs";
3
+ const viewportWidthFlag = '--viewport-width';
4
+ const viewportHeightFlag = '--viewport-height';
5
+ function isLikelyCdpEndpoint(value) {
6
+ return !!value && /^(wss?):\/\//.test(value);
7
+ }
8
+ function parsePositiveIntegerOption(flag, rawValue) {
9
+ const value = Number(rawValue);
10
+ if (!Number.isInteger(value) || value <= 0) throw new CLIError(`Invalid value for "${flag}": expected a positive integer, got "${rawValue}".`);
11
+ return value;
12
+ }
13
+ function readRequiredOptionValue(args, index, flag) {
14
+ const currentArg = args[index];
15
+ const inlinePrefix = `${flag}=`;
16
+ if (currentArg.startsWith(inlinePrefix)) return {
17
+ value: currentArg.slice(inlinePrefix.length),
18
+ nextIndex: index
19
+ };
20
+ const nextArg = args[index + 1];
21
+ if (!nextArg || nextArg.startsWith('--')) throw new CLIError(`Option "${flag}" requires a value.`);
22
+ return {
23
+ value: nextArg,
24
+ nextIndex: index + 1
25
+ };
26
+ }
27
+ function readOptionalCdpEndpoint(args, index) {
28
+ const currentArg = args[index];
29
+ const inlinePrefix = '--cdp=';
30
+ if (currentArg.startsWith(inlinePrefix)) return {
31
+ value: currentArg.slice(inlinePrefix.length),
32
+ nextIndex: index
33
+ };
34
+ const nextArg = args[index + 1];
35
+ if (!isLikelyCdpEndpoint(nextArg)) return {
36
+ nextIndex: index
37
+ };
38
+ return {
39
+ value: nextArg,
40
+ nextIndex: index + 1
41
+ };
42
+ }
43
+ function parseWebCliOptions(rawArgs, env = process.env) {
44
+ const argv = [];
45
+ let isBridge = false;
46
+ let isCdp = false;
47
+ let viewportWidth;
48
+ let viewportHeight;
49
+ let cdpEndpoint;
50
+ for(let index = 0; index < rawArgs.length; index += 1){
51
+ const arg = rawArgs[index];
52
+ if ('--bridge' === arg) {
53
+ isBridge = true;
54
+ continue;
55
+ }
56
+ if ('--cdp' === arg || arg.startsWith('--cdp=')) {
57
+ isCdp = true;
58
+ const parsed = readOptionalCdpEndpoint(rawArgs, index);
59
+ cdpEndpoint = parsed.value ?? cdpEndpoint;
60
+ index = parsed.nextIndex;
61
+ continue;
62
+ }
63
+ if (arg === viewportWidthFlag || arg.startsWith(`${viewportWidthFlag}=`)) {
64
+ const parsed = readRequiredOptionValue(rawArgs, index, viewportWidthFlag);
65
+ viewportWidth = parsePositiveIntegerOption(viewportWidthFlag, parsed.value);
66
+ index = parsed.nextIndex;
67
+ continue;
68
+ }
69
+ if (arg === viewportHeightFlag || arg.startsWith(`${viewportHeightFlag}=`)) {
70
+ const parsed = readRequiredOptionValue(rawArgs, index, viewportHeightFlag);
71
+ viewportHeight = parsePositiveIntegerOption(viewportHeightFlag, parsed.value);
72
+ index = parsed.nextIndex;
73
+ continue;
74
+ }
75
+ argv.push(arg);
76
+ }
77
+ if (isBridge && isCdp) throw new CLIError('--bridge and --cdp are mutually exclusive. Please specify only one.');
78
+ const mode = isBridge ? 'bridge' : isCdp ? 'cdp' : 'puppeteer';
79
+ if ('puppeteer' !== mode) {
80
+ if (void 0 !== viewportWidth || void 0 !== viewportHeight) throw new CLIError('Viewport options are only supported in the default Puppeteer mode.');
81
+ }
82
+ if ('cdp' === mode) {
83
+ cdpEndpoint = cdpEndpoint ?? env.MIDSCENE_CDP_ENDPOINT;
84
+ if (!cdpEndpoint) throw new CLIError('CDP endpoint is required. Provide it as: --cdp <ws-endpoint> or set MIDSCENE_CDP_ENDPOINT environment variable.');
85
+ }
86
+ const hasViewportOverride = void 0 !== viewportWidth || void 0 !== viewportHeight;
87
+ return {
88
+ argv,
89
+ mode,
90
+ cdpEndpoint,
91
+ viewport: hasViewportOverride ? resolveViewportSize({
92
+ width: viewportWidth,
93
+ height: viewportHeight
94
+ }, defaultPuppeteerWindowViewportSize) : void 0
95
+ };
96
+ }
97
+ export { parseWebCliOptions };
@@ -0,0 +1,26 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createReportCliCommands } from "@godscene/core";
4
+ import { reportCLIError, runToolsCLI } from "@godscene/shared/cli";
5
+ import dotenv from "dotenv";
6
+ 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
+ const envFile = join(process.cwd(), '.env');
11
+ if (existsSync(envFile)) dotenv.config({
12
+ path: envFile
13
+ });
14
+ Promise.resolve().then(()=>{
15
+ const parsedOptions = parseWebCliOptions(process.argv.slice(2));
16
+ let tools;
17
+ tools = 'bridge' === parsedOptions.mode ? new WebMidsceneTools() : 'cdp' === parsedOptions.mode ? new WebCdpMidsceneTools(parsedOptions.cdpEndpoint) : new WebPuppeteerMidsceneTools(parsedOptions.viewport);
18
+ return runToolsCLI(tools, 'midscene-web', {
19
+ stripPrefix: 'web_',
20
+ argv: parsedOptions.argv,
21
+ version: "1.7.10",
22
+ extraCommands: createReportCliCommands()
23
+ });
24
+ }).catch((e)=>{
25
+ process.exit(reportCLIError(e));
26
+ });
@@ -0,0 +1,26 @@
1
+ import { AiJudgeOrderSensitive, callAIWithObjectResponse } from "@godscene/core/ai-model";
2
+ const sanitizeXpaths = (xpaths)=>{
3
+ if (!Array.isArray(xpaths)) return [];
4
+ return xpaths.filter((xpath)=>'string' == typeof xpath && xpath.length > 0);
5
+ };
6
+ async function judgeOrderSensitive(options, debug) {
7
+ if (!options?.targetDescription || !options?.modelConfig) return false;
8
+ try {
9
+ const judgeResult = await AiJudgeOrderSensitive(options.targetDescription, callAIWithObjectResponse, options.modelConfig);
10
+ debug("judged isOrderSensitive=%s for description: %s", judgeResult.isOrderSensitive, options.targetDescription);
11
+ return judgeResult.isOrderSensitive;
12
+ } catch (error) {
13
+ debug('Failed to judge isOrderSensitive: %O', error);
14
+ return false;
15
+ }
16
+ }
17
+ function buildRectFromElementInfo(elementInfo) {
18
+ const matchedRect = {
19
+ left: elementInfo.rect.left,
20
+ top: elementInfo.rect.top,
21
+ width: elementInfo.rect.width,
22
+ height: elementInfo.rect.height
23
+ };
24
+ return matchedRect;
25
+ }
26
+ export { buildRectFromElementInfo, judgeOrderSensitive, sanitizeXpaths };
@@ -0,0 +1,36 @@
1
+ import { assert } from "@godscene/shared/utils";
2
+ const defaultViewportWidth = 1440;
3
+ const defaultViewportHeight = 800;
4
+ const defaultViewportSize = {
5
+ width: defaultViewportWidth,
6
+ height: defaultViewportHeight
7
+ };
8
+ const defaultPuppeteerWindowViewportSize = {
9
+ width: defaultViewportWidth,
10
+ height: defaultViewportHeight
11
+ };
12
+ const defaultStaticPageViewportSize = {
13
+ width: defaultViewportWidth,
14
+ height: defaultViewportHeight
15
+ };
16
+ function parseViewportDimension(rawValue, name) {
17
+ const parsedValue = 'number' == typeof rawValue ? rawValue : Number(rawValue);
18
+ assert(Number.isInteger(parsedValue), `${name} must be a positive integer, but got ${rawValue}`);
19
+ assert(parsedValue > 0, `${name} must be greater than 0, but got ${rawValue}`);
20
+ return parsedValue;
21
+ }
22
+ function resolveViewportSize(viewport, fallback = defaultViewportSize) {
23
+ const width = viewport?.width === void 0 || null === viewport.width ? fallback.width : parseViewportDimension(viewport.width, 'viewportWidth');
24
+ const height = viewport?.height === void 0 || null === viewport.height ? fallback.height : parseViewportDimension(viewport.height, 'viewportHeight');
25
+ return {
26
+ width,
27
+ height
28
+ };
29
+ }
30
+ function resolveWebViewportSize(viewport, fallback = defaultViewportSize) {
31
+ return resolveViewportSize({
32
+ width: viewport?.viewportWidth,
33
+ height: viewport?.viewportHeight
34
+ }, fallback);
35
+ }
36
+ export { defaultPuppeteerWindowViewportSize, defaultStaticPageViewportSize, defaultViewportHeight, defaultViewportSize, defaultViewportWidth, resolveViewportSize, resolveWebViewportSize };
@@ -0,0 +1,8 @@
1
+ import { PlaywrightAgent, PlaywrightAiFixture } from "./playwright/index.mjs";
2
+ import { Agent } from "@godscene/core/agent";
3
+ import { PuppeteerAgent } from "./puppeteer/index.mjs";
4
+ import { StaticPage, StaticPageAgent } from "./static/index.mjs";
5
+ import { WebMidsceneTools } from "./mcp-tools.mjs";
6
+ import { webPlaygroundPlatform } from "./platform.mjs";
7
+ import { WebCdpMidsceneTools } from "./mcp-tools-cdp.mjs";
8
+ export { Agent as PageAgent, PlaywrightAgent, PlaywrightAiFixture, PuppeteerAgent, StaticPage, StaticPageAgent, WebCdpMidsceneTools, WebMidsceneTools, webPlaygroundPlatform };
@@ -0,0 +1,33 @@
1
+ import { BaseMCPServer, createMCPServerLauncher } from "@godscene/shared/mcp";
2
+ import { WebMidsceneTools } from "./mcp-tools.mjs";
3
+ class WebMCPServer extends BaseMCPServer {
4
+ createToolsManager() {
5
+ return new WebMidsceneTools();
6
+ }
7
+ constructor(toolsManager){
8
+ super({
9
+ name: '@godscene/web-bridge-mcp',
10
+ version: "1.7.10",
11
+ description: 'Control the browser using natural language commands'
12
+ }, toolsManager);
13
+ }
14
+ }
15
+ function mcpServerForAgent(agent) {
16
+ return createMCPServerLauncher({
17
+ agent,
18
+ platformName: 'Web',
19
+ ToolsManagerClass: WebMidsceneTools,
20
+ MCPServerClass: WebMCPServer
21
+ });
22
+ }
23
+ async function mcpKitForAgent(agent) {
24
+ const toolsManager = new WebMidsceneTools();
25
+ const webAgent = agent;
26
+ toolsManager.setAgent(webAgent);
27
+ await toolsManager.initTools();
28
+ return {
29
+ description: 'Midscene Bridge MCP Server: Control the browser using natural language commands for navigation, clicking, input, hovering, screenshots waitFor, and achieving goals.',
30
+ tools: toolsManager.getToolDefinitions()
31
+ };
32
+ }
33
+ export { WebMCPServer, mcpKitForAgent, mcpServerForAgent };
@@ -0,0 +1,164 @@
1
+ import { ScreenshotItem, z } from "@godscene/core";
2
+ import { getDebug } from "@godscene/shared/logger";
3
+ import { BaseMidsceneTools } from "@godscene/shared/mcp/base-tools";
4
+ import puppeteer_core from "puppeteer-core";
5
+ import { getProxyEndpoint } from "./cdp-proxy-manager.mjs";
6
+ import { cleanupTargetIdFile, readSavedTargetId, saveTargetId } from "./cdp-target-store.mjs";
7
+ import { defaultStaticPageViewportSize } from "./common/viewport.mjs";
8
+ import { PuppeteerAgent } from "./puppeteer/index.mjs";
9
+ import { StaticPage } from "./static/index.mjs";
10
+ function _define_property(obj, key, value) {
11
+ if (key in obj) Object.defineProperty(obj, key, {
12
+ value: value,
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true
16
+ });
17
+ else obj[key] = value;
18
+ return obj;
19
+ }
20
+ const debug = getDebug('mcp:cdp');
21
+ const CDP_TARGET_DISCOVERY_DELAY_MS = 500;
22
+ function getTargetId(page) {
23
+ return page.target()._targetId;
24
+ }
25
+ class WebCdpMidsceneTools extends BaseMidsceneTools {
26
+ getCliReportSessionName() {
27
+ return 'midscene-web';
28
+ }
29
+ createTemporaryDevice() {
30
+ return new StaticPage({
31
+ screenshot: ScreenshotItem.create('', Date.now()),
32
+ shotSize: defaultStaticPageViewportSize,
33
+ shrunkShotToLogicalRatio: 1
34
+ });
35
+ }
36
+ async ensureAgent(navigateToUrl) {
37
+ if (this.agent && navigateToUrl) {
38
+ try {
39
+ await this.agent?.destroy?.();
40
+ } catch (error) {
41
+ console.debug('Failed to destroy agent during re-init:', error);
42
+ }
43
+ this.agent = void 0;
44
+ }
45
+ if (this.agent) return this.agent;
46
+ if (!this.activeBrowser) {
47
+ const endpoint = await getProxyEndpoint(this.cdpEndpoint);
48
+ this.activeBrowser = await puppeteer_core.connect({
49
+ browserWSEndpoint: endpoint,
50
+ defaultViewport: null
51
+ });
52
+ }
53
+ const browser = this.activeBrowser;
54
+ let pages = await browser.pages();
55
+ if (0 === pages.length) {
56
+ await new Promise((r)=>setTimeout(r, CDP_TARGET_DISCOVERY_DELAY_MS));
57
+ pages = await browser.pages();
58
+ }
59
+ const webPages = pages.filter((p)=>/^https?:\/\//.test(p.url()));
60
+ debug('Found %d page(s), %d web page(s): %o', pages.length, webPages.length, pages.map((p)=>p.url()));
61
+ let page;
62
+ if (navigateToUrl) if (webPages.length > 0) {
63
+ page = webPages[webPages.length - 1];
64
+ await page.bringToFront();
65
+ await page.goto(navigateToUrl, {
66
+ timeout: 30000,
67
+ waitUntil: 'domcontentloaded'
68
+ });
69
+ } else {
70
+ page = await browser.newPage();
71
+ await page.goto(navigateToUrl, {
72
+ timeout: 30000,
73
+ waitUntil: 'domcontentloaded'
74
+ });
75
+ }
76
+ else {
77
+ const savedTargetId = readSavedTargetId();
78
+ let matchedPage;
79
+ if (savedTargetId && pages.length > 0) {
80
+ matchedPage = pages.find((p)=>getTargetId(p) === savedTargetId);
81
+ matchedPage ? debug('Matched saved targetId %s', savedTargetId) : debug('Saved targetId %s not found among %d pages, falling back', savedTargetId, pages.length);
82
+ }
83
+ page = matchedPage ? matchedPage : webPages.length > 0 ? webPages[webPages.length - 1] : pages.length > 0 ? pages[pages.length - 1] : await browser.newPage();
84
+ await page.bringToFront();
85
+ }
86
+ const targetId = getTargetId(page);
87
+ if (targetId) saveTargetId(targetId);
88
+ else debug('No targetId on page.target(); cross-command tab reuse disabled until puppeteer integration is updated.');
89
+ const reportOptions = this.readCliReportAgentOptions();
90
+ this.agent = new PuppeteerAgent(page, {
91
+ ...reportOptions ?? {}
92
+ });
93
+ return this.agent;
94
+ }
95
+ async destroy() {
96
+ await super.destroy();
97
+ if (this.activeBrowser) {
98
+ this.activeBrowser.disconnect();
99
+ this.activeBrowser = null;
100
+ }
101
+ }
102
+ preparePlatformTools() {
103
+ return [
104
+ {
105
+ name: 'web_connect',
106
+ description: 'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',
107
+ schema: {
108
+ url: z.string().url().optional().describe('URL to open in new tab (omit to use current page)')
109
+ },
110
+ handler: async (args)=>{
111
+ const { url } = args;
112
+ if (this.agent) {
113
+ try {
114
+ await this.agent.destroy?.();
115
+ } catch (e) {
116
+ console.debug('Failed to destroy agent during connect:', e);
117
+ }
118
+ this.agent = void 0;
119
+ }
120
+ const reportSession = this.createNewCliReportSession(url ?? 'current-page');
121
+ this.commitCliReportSession(reportSession);
122
+ this.agent = await this.ensureAgent(url);
123
+ const screenshot = await this.agent.page?.screenshotBase64();
124
+ const label = url ?? 'current page';
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: `Connected via CDP to: ${label}`
130
+ },
131
+ ...screenshot ? this.buildScreenshotContent(screenshot) : []
132
+ ]
133
+ };
134
+ }
135
+ },
136
+ {
137
+ name: 'web_disconnect',
138
+ description: 'Disconnect from current web page. The browser stays running (managed externally).',
139
+ schema: {},
140
+ handler: async ()=>{
141
+ if (this.agent) {
142
+ try {
143
+ await this.agent.destroy?.();
144
+ } catch (e) {
145
+ console.debug('Failed to destroy agent during disconnect:', e);
146
+ }
147
+ this.agent = void 0;
148
+ }
149
+ if (this.activeBrowser) {
150
+ this.activeBrowser.disconnect();
151
+ this.activeBrowser = null;
152
+ }
153
+ cleanupTargetIdFile();
154
+ return this.buildTextResult('Disconnected from web page (browser still running externally)');
155
+ }
156
+ }
157
+ ];
158
+ }
159
+ constructor(cdpEndpoint){
160
+ super(), _define_property(this, "cdpEndpoint", void 0), _define_property(this, "activeBrowser", null);
161
+ this.cdpEndpoint = cdpEndpoint;
162
+ }
163
+ }
164
+ export { WebCdpMidsceneTools };
@@ -0,0 +1,246 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { ScreenshotItem, z } from "@godscene/core";
7
+ import { BaseMidsceneTools } from "@godscene/shared/mcp/base-tools";
8
+ import { resolveChromePath } from "@godscene/shared/mcp/chrome-path";
9
+ import puppeteer_core from "puppeteer-core";
10
+ import { defaultPuppeteerWindowViewportSize, defaultStaticPageViewportSize } from "./common/viewport.mjs";
11
+ import { PuppeteerAgent } from "./puppeteer/index.mjs";
12
+ import { StaticPage } from "./static/index.mjs";
13
+ function _define_property(obj, key, value) {
14
+ if (key in obj) Object.defineProperty(obj, key, {
15
+ value: value,
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true
19
+ });
20
+ else obj[key] = value;
21
+ return obj;
22
+ }
23
+ const ENDPOINT_FILE = join(tmpdir(), 'midscene-puppeteer-endpoint');
24
+ const USER_DATA_DIR = join(tmpdir(), 'midscene-puppeteer-profile');
25
+ const PUPPETEER_ENDPOINT_FILE = ENDPOINT_FILE;
26
+ function buildDetachedChromeArgs(options) {
27
+ const viewport = options.viewport ?? defaultPuppeteerWindowViewportSize;
28
+ return [
29
+ '--headless=new',
30
+ `--user-data-dir=${options.userDataDir}`,
31
+ '--remote-debugging-port=0',
32
+ '--no-first-run',
33
+ '--no-default-browser-check',
34
+ '--disable-extensions',
35
+ '--disable-default-apps',
36
+ '--disable-sync',
37
+ '--disable-background-networking',
38
+ '--password-store=basic',
39
+ '--use-mock-keychain',
40
+ `--window-size=${viewport.width},${viewport.height}`,
41
+ '--force-color-profile=srgb'
42
+ ];
43
+ }
44
+ const browserManager = {
45
+ activeBrowser: null,
46
+ async getOrLaunch (viewport) {
47
+ if (existsSync(ENDPOINT_FILE)) try {
48
+ const endpoint = (await readFile(ENDPOINT_FILE, 'utf-8')).trim();
49
+ const browser = await puppeteer_core.connect({
50
+ browserWSEndpoint: endpoint,
51
+ defaultViewport: null
52
+ });
53
+ return {
54
+ browser,
55
+ reused: true
56
+ };
57
+ } catch {
58
+ try {
59
+ await unlink(ENDPOINT_FILE);
60
+ } catch {}
61
+ }
62
+ const wsEndpoint = await this.launchDetachedChrome(viewport);
63
+ await writeFile(ENDPOINT_FILE, wsEndpoint);
64
+ const browser = await puppeteer_core.connect({
65
+ browserWSEndpoint: wsEndpoint,
66
+ defaultViewport: null
67
+ });
68
+ return {
69
+ browser,
70
+ reused: false
71
+ };
72
+ },
73
+ async closeBrowser () {
74
+ if (!existsSync(ENDPOINT_FILE)) return;
75
+ try {
76
+ const endpoint = (await readFile(ENDPOINT_FILE, 'utf-8')).trim();
77
+ const browser = await puppeteer_core.connect({
78
+ browserWSEndpoint: endpoint
79
+ });
80
+ await browser.close();
81
+ } catch {}
82
+ try {
83
+ await unlink(ENDPOINT_FILE);
84
+ } catch {}
85
+ },
86
+ disconnect () {
87
+ if (this.activeBrowser) {
88
+ this.activeBrowser.disconnect();
89
+ this.activeBrowser = null;
90
+ }
91
+ },
92
+ async launchDetachedChrome (viewport) {
93
+ const chromePath = resolveChromePath();
94
+ await mkdir(USER_DATA_DIR, {
95
+ recursive: true
96
+ });
97
+ const args = buildDetachedChromeArgs({
98
+ userDataDir: USER_DATA_DIR,
99
+ viewport
100
+ });
101
+ const proc = spawn(chromePath, args, {
102
+ detached: true,
103
+ stdio: [
104
+ 'ignore',
105
+ 'ignore',
106
+ 'pipe'
107
+ ]
108
+ });
109
+ proc.unref();
110
+ return new Promise((resolve, reject)=>{
111
+ let output = '';
112
+ const onData = (data)=>{
113
+ output += data.toString();
114
+ const match = output.match(/DevTools listening on (ws:\/\/[^\s]+)/);
115
+ if (match) {
116
+ proc.stderr.removeListener('data', onData);
117
+ resolve(match[1]);
118
+ }
119
+ };
120
+ proc.stderr.on('data', onData);
121
+ proc.on('exit', (code)=>{
122
+ proc.stderr.removeListener('data', onData);
123
+ reject(new Error(`Chrome exited with code ${code} before DevTools was ready.\nChrome stderr: ${output}\nTip: try setting MIDSCENE_MCP_NO_SANDBOX=1 if running in a container.`));
124
+ });
125
+ setTimeout(()=>reject(new Error(`Chrome launch timeout.\nChrome stderr: ${output}\nTip: try setting MIDSCENE_MCP_NO_SANDBOX=1 if running in a container.`)), 15000);
126
+ });
127
+ }
128
+ };
129
+ class WebPuppeteerMidsceneTools extends BaseMidsceneTools {
130
+ getCliReportSessionName() {
131
+ return 'midscene-web';
132
+ }
133
+ createTemporaryDevice() {
134
+ return new StaticPage({
135
+ screenshot: ScreenshotItem.create('', Date.now()),
136
+ shotSize: this.viewport ?? defaultStaticPageViewportSize,
137
+ shrunkShotToLogicalRatio: 1
138
+ });
139
+ }
140
+ async ensureAgent(navigateToUrl) {
141
+ if (this.agent && navigateToUrl) {
142
+ try {
143
+ await this.agent?.destroy?.();
144
+ } catch {}
145
+ this.agent = void 0;
146
+ }
147
+ if (this.agent) return this.agent;
148
+ const { browser, reused } = await browserManager.getOrLaunch(this.viewport);
149
+ browserManager.activeBrowser = browser;
150
+ const pages = await browser.pages();
151
+ let page;
152
+ if (navigateToUrl) {
153
+ page = await browser.newPage();
154
+ if (this.viewport) await page.setViewport(this.viewport);
155
+ await page.goto(navigateToUrl, {
156
+ timeout: 30000,
157
+ waitUntil: 'domcontentloaded'
158
+ });
159
+ } else {
160
+ const webPages = pages.filter((p)=>/^https?:\/\//.test(p.url()));
161
+ page = webPages.length > 0 ? webPages[webPages.length - 1] : pages[pages.length - 1] || await browser.newPage();
162
+ if (reused) await page.bringToFront();
163
+ if (this.viewport) await page.setViewport(this.viewport);
164
+ }
165
+ const reportOptions = this.readCliReportAgentOptions();
166
+ this.agent = new PuppeteerAgent(page, {
167
+ ...reportOptions ?? {}
168
+ });
169
+ return this.agent;
170
+ }
171
+ async destroy() {
172
+ await super.destroy();
173
+ browserManager.disconnect();
174
+ }
175
+ preparePlatformTools() {
176
+ return [
177
+ {
178
+ name: 'web_connect',
179
+ description: 'Connect to a web page. Opens a new tab with the given URL, or reuses the current page.',
180
+ schema: {
181
+ url: z.string().url().optional().describe('URL to open in new tab (omit to use current page)')
182
+ },
183
+ handler: async (args)=>{
184
+ const { url } = args;
185
+ if (this.agent) {
186
+ try {
187
+ await this.agent.destroy?.();
188
+ } catch {}
189
+ this.agent = void 0;
190
+ }
191
+ const reportSession = this.createNewCliReportSession(url ?? 'current-page');
192
+ this.commitCliReportSession(reportSession);
193
+ this.agent = await this.ensureAgent(url);
194
+ const screenshot = await this.agent.page?.screenshotBase64();
195
+ const label = url ?? 'current page';
196
+ return {
197
+ content: [
198
+ {
199
+ type: 'text',
200
+ text: `Connected to: ${label}`
201
+ },
202
+ ...screenshot ? this.buildScreenshotContent(screenshot) : []
203
+ ]
204
+ };
205
+ }
206
+ },
207
+ {
208
+ name: 'web_disconnect',
209
+ description: 'Disconnect from current web page. The browser stays running for future calls.',
210
+ schema: {},
211
+ handler: async ()=>{
212
+ if (this.agent) {
213
+ try {
214
+ await this.agent.destroy?.();
215
+ } catch {}
216
+ this.agent = void 0;
217
+ }
218
+ browserManager.disconnect();
219
+ return this.buildTextResult('Disconnected from web page (browser still running)');
220
+ }
221
+ },
222
+ {
223
+ name: 'web_close',
224
+ description: 'Close the browser completely and release all resources.',
225
+ schema: {},
226
+ handler: async ()=>{
227
+ if (this.agent) {
228
+ try {
229
+ await this.agent.destroy?.();
230
+ } catch {}
231
+ this.agent = void 0;
232
+ }
233
+ await browserManager.closeBrowser();
234
+ return this.buildTextResult('Browser closed');
235
+ }
236
+ }
237
+ ];
238
+ }
239
+ constructor(viewport){
240
+ super(), _define_property(this, "viewport", void 0);
241
+ this.viewport = viewport ? {
242
+ ...viewport
243
+ } : void 0;
244
+ }
245
+ }
246
+ export { PUPPETEER_ENDPOINT_FILE, WebPuppeteerMidsceneTools, buildDetachedChromeArgs };