@skyramp/mcp 0.1.8 → 0.2.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var import_widgetContract = require("./widgetContract");
3
+ var import_curatedWidgets = require("./curatedWidgets");
4
+ const cases = [];
5
+ function test(name, run) {
6
+ cases.push({ name, run });
7
+ }
8
+ function assertEqual(actual, expected, msg) {
9
+ const a = JSON.stringify(actual);
10
+ const e = JSON.stringify(expected);
11
+ if (a !== e)
12
+ throw new Error(`${msg ?? "assertEqual"} \u2014 expected ${e}, got ${a}`);
13
+ }
14
+ function assertThrows(fn, substring) {
15
+ let threw = false;
16
+ let message = "";
17
+ try {
18
+ fn();
19
+ } catch (e) {
20
+ threw = true;
21
+ message = e.message;
22
+ }
23
+ if (!threw) throw new Error("expected function to throw");
24
+ if (substring && !message.includes(substring))
25
+ throw new Error(`thrown message "${message}" does not include "${substring}"`);
26
+ }
27
+ const curatedFingerprints = Object.keys(import_curatedWidgets.CURATED);
28
+ const firstCurated = curatedFingerprints[0];
29
+ test("lookupContract: curated takes precedence over inferred", () => {
30
+ const cache = /* @__PURE__ */ new Map();
31
+ cache.set(firstCurated, {
32
+ source: "inferred",
33
+ confidence: "low",
34
+ widgetIdentification: "cache-shadow attempt",
35
+ parameters: [],
36
+ steps: []
37
+ });
38
+ const result = (0, import_widgetContract.lookupContract)(firstCurated, cache);
39
+ if (result.status !== "found") throw new Error("expected found");
40
+ assertEqual(result.contract.source, "curated");
41
+ });
42
+ test("lookupContract: inferred returned when no curated entry exists", () => {
43
+ const cache = /* @__PURE__ */ new Map();
44
+ const inferred = {
45
+ source: "inferred",
46
+ confidence: "high",
47
+ widgetIdentification: "CustomDatePicker",
48
+ parameters: ["date"],
49
+ steps: [{ kind: "click", target: "trigger" }]
50
+ };
51
+ cache.set("0000000000000001", inferred);
52
+ const result = (0, import_widgetContract.lookupContract)("0000000000000001", cache);
53
+ if (result.status !== "found") throw new Error("expected found");
54
+ assertEqual(result.contract.source, "inferred");
55
+ });
56
+ test("lookupContract: unknown fingerprint returns needs_inference_marker", () => {
57
+ const cache = /* @__PURE__ */ new Map();
58
+ const result = (0, import_widgetContract.lookupContract)("ffffffffffffffff", cache);
59
+ assertEqual(result.status, "needs_inference_marker");
60
+ });
61
+ test("cacheInferredContract: refuses to overwrite curated entry", () => {
62
+ const cache = /* @__PURE__ */ new Map();
63
+ const result = (0, import_widgetContract.cacheInferredContract)(firstCurated, {
64
+ source: "inferred",
65
+ confidence: "high",
66
+ widgetIdentification: "X",
67
+ parameters: [],
68
+ steps: []
69
+ }, cache);
70
+ assertEqual(result, { ok: false, error: "fingerprint collides with curated entry; cannot overwrite" });
71
+ });
72
+ test("cacheInferredContract: accepts valid inferred contract", () => {
73
+ const cache = /* @__PURE__ */ new Map();
74
+ const result = (0, import_widgetContract.cacheInferredContract)("0000000000000002", {
75
+ source: "inferred",
76
+ confidence: "high",
77
+ widgetIdentification: "X",
78
+ parameters: ["v"],
79
+ steps: [{ kind: "fill", target: "inputByLabel", label: "Name", param: "v" }]
80
+ }, cache);
81
+ assertEqual(result, { ok: true });
82
+ assertEqual(cache.size, 1);
83
+ });
84
+ test("cacheInferredContract: overwrites previous inferred contract (mutable)", () => {
85
+ const cache = /* @__PURE__ */ new Map();
86
+ (0, import_widgetContract.cacheInferredContract)("0000000000000003", {
87
+ source: "inferred",
88
+ confidence: "low",
89
+ widgetIdentification: "bad",
90
+ parameters: [],
91
+ steps: []
92
+ }, cache);
93
+ (0, import_widgetContract.cacheInferredContract)("0000000000000003", {
94
+ source: "inferred",
95
+ confidence: "high",
96
+ widgetIdentification: "good",
97
+ parameters: [],
98
+ steps: []
99
+ }, cache);
100
+ assertEqual(cache.get("0000000000000003").widgetIdentification, "good");
101
+ });
102
+ test("cacheInferredContract: accepts unknown stub", () => {
103
+ const cache = /* @__PURE__ */ new Map();
104
+ const result = (0, import_widgetContract.cacheInferredContract)("0000000000000004", {
105
+ source: "unknown",
106
+ confidence: "low",
107
+ widgetIdentification: "",
108
+ parameters: [],
109
+ steps: []
110
+ }, cache);
111
+ assertEqual(result, { ok: true });
112
+ });
113
+ test("cacheInferredContract: rejects source=curated (defense in depth)", () => {
114
+ const cache = /* @__PURE__ */ new Map();
115
+ const result = (0, import_widgetContract.cacheInferredContract)("0000000000000005", {
116
+ source: "curated",
117
+ confidence: "high",
118
+ widgetIdentification: "forged",
119
+ parameters: [],
120
+ steps: []
121
+ }, cache);
122
+ if (result.ok)
123
+ throw new Error("expected reject when source=curated");
124
+ if (!result.error.includes("source: 'curated'"))
125
+ throw new Error(`expected error message to mention curated, got: ${result.error}`);
126
+ assertEqual(cache.size, 0);
127
+ });
128
+ test("validateContract: step.param must exist in parameters[]", () => {
129
+ const c = {
130
+ source: "inferred",
131
+ confidence: "high",
132
+ widgetIdentification: "X",
133
+ parameters: ["v"],
134
+ steps: [{ kind: "click", target: "option", matchBy: "accessibleName", param: "w" }]
135
+ };
136
+ assertThrows(() => (0, import_widgetContract.validateContract)(c), "param 'w'");
137
+ });
138
+ test("validateContract: EnterFrameStep.framePath must be non-empty", () => {
139
+ const c = {
140
+ source: "inferred",
141
+ confidence: "high",
142
+ widgetIdentification: "X",
143
+ parameters: [],
144
+ steps: [{ kind: "enterFrame", framePath: [] }, { kind: "exitFrame" }]
145
+ };
146
+ assertThrows(() => (0, import_widgetContract.validateContract)(c), "framePath");
147
+ });
148
+ test("validateContract: rule 1 (missing param) is reported before rule 2 (empty framePath)", () => {
149
+ const c = {
150
+ source: "inferred",
151
+ confidence: "high",
152
+ widgetIdentification: "X",
153
+ parameters: [],
154
+ steps: [
155
+ { kind: "click", target: "option", matchBy: "accessibleName", param: "v" },
156
+ { kind: "enterFrame", framePath: [] }
157
+ ]
158
+ };
159
+ assertThrows(() => (0, import_widgetContract.validateContract)(c), "param 'v'");
160
+ });
161
+ test("validateContract: at most one EnterFrameStep", () => {
162
+ const c = {
163
+ source: "inferred",
164
+ confidence: "high",
165
+ widgetIdentification: "X",
166
+ parameters: [],
167
+ steps: [
168
+ { kind: "enterFrame", framePath: ["iframe"] },
169
+ { kind: "exitFrame" },
170
+ { kind: "enterFrame", framePath: ["iframe"] }
171
+ ]
172
+ };
173
+ assertThrows(() => (0, import_widgetContract.validateContract)(c), "EnterFrameStep");
174
+ });
175
+ test("validateContract: HandoffStep-before-WaitForStep is a warning, not an error", () => {
176
+ const c = {
177
+ source: "inferred",
178
+ confidence: "high",
179
+ widgetIdentification: "X",
180
+ parameters: [],
181
+ steps: [{ kind: "click", target: "trigger" }, { kind: "handoff" }]
182
+ };
183
+ (0, import_widgetContract.validateContract)(c);
184
+ });
185
+ test("validateContract: widgetIdentification length is NOT enforced here (Zod schema concern)", () => {
186
+ const c = {
187
+ source: "inferred",
188
+ confidence: "high",
189
+ widgetIdentification: "a".repeat(200),
190
+ parameters: [],
191
+ steps: []
192
+ };
193
+ (0, import_widgetContract.validateContract)(c);
194
+ });
195
+ let failed = 0;
196
+ for (const { name, run } of cases) {
197
+ try {
198
+ run();
199
+ console.log(" \u2713", name);
200
+ } catch (e) {
201
+ failed++;
202
+ console.log(" \u2717", name);
203
+ console.log(" ", e.message);
204
+ }
205
+ }
206
+ if (failed > 0) {
207
+ console.log(`
208
+ ${failed}/${cases.length} failed`);
209
+ process.exit(1);
210
+ }
211
+ console.log(`
212
+ ${cases.length} passed`);
@@ -184,6 +184,7 @@ class PersistentContextFactory {
184
184
  (0, import_log.testDebug)("lock user data dir", userDataDir);
185
185
  const browserType = playwright[this.config.browser.browserName];
186
186
  for (let i = 0; i < 5; i++) {
187
+ const hasLoadExtension = this.config.browser.launchOptions?.args?.some((arg) => arg.startsWith("--load-extension=")) ?? false;
187
188
  const launchOptions = {
188
189
  tracesDir,
189
190
  ...this.config.browser.launchOptions,
@@ -191,7 +192,8 @@ class PersistentContextFactory {
191
192
  handleSIGINT: false,
192
193
  handleSIGTERM: false,
193
194
  ignoreDefaultArgs: [
194
- "--disable-extensions"
195
+ "--disable-extensions",
196
+ ...hasLoadExtension ? ["--disable-component-extensions-with-background-pages"] : []
195
197
  ],
196
198
  assistantMode: true,
197
199
  ...options.forceHeadless !== void 0 ? { headless: options.forceHeadless === "headless" } : {}
@@ -248,7 +248,7 @@ function configFromEnv() {
248
248
  options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
249
249
  options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
250
250
  options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
251
- options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);
251
+ options.userDataDir = envToString(process.env.PLAYWRIGHT_USER_DATA_DIR);
252
252
  options.viewportSize = resolutionParser("--viewport-size", process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
253
253
  return configFromCLIOptions(options);
254
254
  }
@@ -45,6 +45,18 @@ class Context {
45
45
  constructor(options) {
46
46
  this._tabs = [];
47
47
  this._abortController = new AbortController();
48
+ // DOM Analyzer state — stored per session so the LLM can query it lazily.
49
+ // Keyed by (normalizedEntryUrl, storageStateHash, probeButtonsMode) —
50
+ // see crawler.ts and tools/sitemap.ts for cache-key composition.
51
+ this.sitemapCache = /* @__PURE__ */ new Map();
52
+ /**
53
+ * Phase B widget-contract inferred cache — per-session, in-memory.
54
+ * Key: 16-hex fingerprint. Value: agent-authored WidgetContract.
55
+ * Curated contracts are NOT stored here — they live in curatedWidgets.ts.
56
+ * Mutable for non-curated fingerprints; cacheInferredContract refuses to
57
+ * overwrite curated entries (see widgetContract.ts).
58
+ */
59
+ this.inferredContracts = /* @__PURE__ */ new Map();
48
60
  this.config = options.config;
49
61
  this.sessionLog = options.sessionLog;
50
62
  this.options = options;
@@ -185,7 +197,11 @@ class Context {
185
197
  if (this.sessionLog)
186
198
  await InputRecorder.create(this, browserContext);
187
199
  if (!this.config.allowUnrestrictedFileAccess) {
188
- browserContext._setAllowedProtocols(["http:", "https:", "about:", "data:"]);
200
+ const allowedProtocols = ["http:", "https:", "about:", "data:"];
201
+ const launchArgs = this.config.browser.launchOptions?.args;
202
+ if (launchArgs?.some((arg) => arg.startsWith("--load-extension=")))
203
+ allowedProtocols.push("chrome-extension:");
204
+ browserContext._setAllowedProtocols(allowedProtocols);
189
205
  browserContext._setAllowedDirectories(allRootPaths(this._clientInfo));
190
206
  }
191
207
  await this._setupRequestInterception(browserContext);
@@ -27,10 +27,12 @@ module.exports = __toCommonJS(tab_exports);
27
27
  var import_events = require("events");
28
28
  var import_utils = require("playwright-core/lib/utils");
29
29
  var import_utils2 = require("./tools/utils");
30
+ var import_extensionFrames = require("./tools/extensionFrames");
30
31
  var import_log = require("../log");
31
32
  var import_dialogs = require("./tools/dialogs");
32
33
  var import_files = require("./tools/files");
33
34
  var import_transform = require("../../transform/transform");
35
+ var import_blueprintCache = require("../../dom-analyzer/blueprintCache");
34
36
  const TabEvents = {
35
37
  modalState: "modalState"
36
38
  };
@@ -45,6 +47,19 @@ class Tab extends import_events.EventEmitter {
45
47
  this._needsFullSnapshot = false;
46
48
  this._eventEntries = [];
47
49
  this._recentEventEntries = [];
50
+ /**
51
+ * Refs for elements inside chrome-extension iframes that live under closed
52
+ * shadow DOMs on the host page. Keyed by refs of the form `ef<frameIdx>-<seq>`
53
+ * and populated by captureSnapshot(); consumed by refLocators().
54
+ */
55
+ this._extensionFrameRefs = /* @__PURE__ */ new Map();
56
+ /**
57
+ * Per-tab cache of the most-recent PageBlueprint per URL. Used by
58
+ * browser_blueprint to return a delta against the prior capture at the
59
+ * same URL instead of the full payload. Cleared on tab close (not on
60
+ * navigation — same-URL revisits should reuse the prior blueprint).
61
+ */
62
+ this.blueprintCache = new import_blueprintCache.BlueprintCache(10);
48
63
  this.context = context;
49
64
  this.page = page;
50
65
  this._onPageClose = onPageClose;
@@ -148,6 +163,7 @@ class Tab extends import_events.EventEmitter {
148
163
  }
149
164
  _onClose() {
150
165
  this._clearCollectedArtifacts();
166
+ this.blueprintCache.clear();
151
167
  this._onPageClose(this);
152
168
  }
153
169
  async headerSnapshot() {
@@ -211,6 +227,20 @@ class Tab extends import_events.EventEmitter {
211
227
  if (tabSnapshot) {
212
228
  tabSnapshot.events = this._recentEventEntries;
213
229
  this._recentEventEntries = [];
230
+ try {
231
+ const extSnap = await (0, import_extensionFrames.enumerateExtensionFrames)(this.page);
232
+ if (extSnap.lines.length) {
233
+ tabSnapshot.ariaSnapshot = tabSnapshot.ariaSnapshot ? `${tabSnapshot.ariaSnapshot}
234
+ ${extSnap.lines.join("\n")}` : extSnap.lines.join("\n");
235
+ if (tabSnapshot.ariaSnapshotDiff !== void 0) {
236
+ tabSnapshot.ariaSnapshotDiff = tabSnapshot.ariaSnapshotDiff ? `${tabSnapshot.ariaSnapshotDiff}
237
+ ${extSnap.lines.join("\n")}` : extSnap.lines.join("\n");
238
+ }
239
+ }
240
+ this._extensionFrameRefs = extSnap.refMap;
241
+ } catch {
242
+ this._extensionFrameRefs = /* @__PURE__ */ new Map();
243
+ }
214
244
  }
215
245
  this._needsFullSnapshot = !tabSnapshot;
216
246
  return tabSnapshot ?? {
@@ -248,6 +278,14 @@ class Tab extends import_events.EventEmitter {
248
278
  async refLocators(params) {
249
279
  await this._initializedPromise;
250
280
  return Promise.all(params.map(async (param) => {
281
+ if (param.ref.startsWith("ef")) {
282
+ const ext = this._extensionFrameRefs.get(param.ref);
283
+ if (!ext)
284
+ throw new Error(`Extension-frame ref ${param.ref} not found. Call browser_snapshot to refresh.`);
285
+ const escapedMatch = ext.urlMatch.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
286
+ const resolved = `frames().find(f => f.url().includes('${escapedMatch}')).${ext.expr}`;
287
+ return { locator: ext.locator, resolved };
288
+ }
251
289
  try {
252
290
  let locator = this.page.locator(`aria-ref=${param.ref}`);
253
291
  if (param.element)
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var domAnalyzer_exports = {};
30
+ __export(domAnalyzer_exports, {
31
+ default: () => domAnalyzer_default
32
+ });
33
+ module.exports = __toCommonJS(domAnalyzer_exports);
34
+ var crypto = __toESM(require("crypto"));
35
+ var fs = __toESM(require("fs"));
36
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
37
+ var import_tool = require("./tool");
38
+ var import_crawler = require("../../../dom-analyzer/crawler");
39
+ var import_blueprint = require("../../../dom-analyzer/blueprint");
40
+ var import_serialization = require("../../../dom-analyzer/serialization");
41
+ function hashStorageState(path) {
42
+ if (!path)
43
+ return null;
44
+ try {
45
+ const contents = fs.readFileSync(path, "utf-8");
46
+ return crypto.createHash("sha256").update(contents).digest("hex").slice(0, 16);
47
+ } catch {
48
+ const pathHash = crypto.createHash("sha256").update(path).digest("hex").slice(0, 16);
49
+ return `unreadable:${pathHash}`;
50
+ }
51
+ }
52
+ function cacheKey(entryUrl, storageStateHash, probeButtons) {
53
+ return `${entryUrl}::${storageStateHash ?? "no-auth"}::${probeButtons}`;
54
+ }
55
+ function humanDuration(ms) {
56
+ if (ms < 6e4)
57
+ return `${Math.round(ms / 1e3)}s`;
58
+ return `${Math.round(ms / 6e4)}m`;
59
+ }
60
+ const sitemapBuild = (0, import_tool.defineTool)({
61
+ capability: "core",
62
+ schema: {
63
+ name: "browser_sitemap_build",
64
+ title: "Build Sitemap (graph of PageBlueprints)",
65
+ description: [
66
+ "Crawl an application starting from a URL and build a Sitemap \u2014 a graph of PageBlueprints (one per URL)",
67
+ "connected by navigation edges. The Sitemap is cached in the session.",
68
+ "",
69
+ "Call once at the start of a session. Subsequent calls within the TTL (~30 minutes) reuse the cached",
70
+ "Sitemap unless `refresh: true` is passed. Use `browser_sitemap_query` to read already-crawled pages",
71
+ "rather than re-calling this tool.",
72
+ "",
73
+ "Depth defaults to 5, maxPages to 50 (the real bound in practice).",
74
+ "",
75
+ '\u26A0\uFE0F SAFETY: By default, `probeButtons: "immutable-only"` skips destructive-looking buttons',
76
+ "(Delete / Submit / Place Order / etc.) and buttons inside forms to prevent side effects during",
77
+ 'crawling. Use `probeButtons: "all"` only against dev / staging environments \u2014 it will click every',
78
+ "unique button, which can submit forms, create records, send notifications, or mutate server state."
79
+ ].join("\n"),
80
+ inputSchema: import_mcpBundle.z.object({
81
+ url: import_mcpBundle.z.string().describe("Entry URL to start crawling from"),
82
+ depth: import_mcpBundle.z.number().optional().default(5).describe("Max crawl depth (default: 5)"),
83
+ maxPages: import_mcpBundle.z.number().optional().default(50).describe("Max pages to visit (default: 50)"),
84
+ sameOriginOnly: import_mcpBundle.z.boolean().optional().default(true).describe("Only follow same-origin links (default: true)"),
85
+ probeButtons: import_mcpBundle.z.enum(["immutable-only", "all", "none"]).optional().default("immutable-only").describe('Button-probing safety mode. Default: immutable-only (safe; skips destructive-verb buttons and buttons inside forms). Use "all" only against dev/staging \u2014 probes every button. "none" disables button probing entirely.'),
86
+ playwrightStoragePath: import_mcpBundle.z.string().optional().describe("Path to a Playwright storageState.json file \u2014 cookies, localStorage, sessionStorage per origin. Use this to crawl apps behind a login."),
87
+ refresh: import_mcpBundle.z.boolean().optional().default(false).describe("Force a full re-crawl, bypassing the cache")
88
+ }),
89
+ type: "readOnly"
90
+ },
91
+ handle: async (context, params, response) => {
92
+ const { url, depth, maxPages, sameOriginOnly, probeButtons, playwrightStoragePath, refresh } = params;
93
+ const normalizedUrl = (0, import_serialization.normalizeUrl)(url);
94
+ const storageStateHash = hashStorageState(playwrightStoragePath);
95
+ const probeMode = probeButtons;
96
+ const key = cacheKey(normalizedUrl, storageStateHash, probeMode);
97
+ const cached = context.sitemapCache.get(key);
98
+ if (cached && !refresh) {
99
+ const age = Date.now() - new Date(cached.sitemap.cachedAt).getTime();
100
+ if (age < import_serialization.CRAWL_TTL_MS) {
101
+ const pageCount2 = Object.keys(cached.sitemap.pages).length;
102
+ response.addTextResult(
103
+ `Reusing cached Sitemap from ${humanDuration(age)} ago.
104
+ Entry: ${cached.sitemap.entryUrl}
105
+ Pages: ${pageCount2}
106
+ Edges: ${cached.sitemap.edges.length}
107
+ Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
108
+ Probe mode: ${probeMode}
109
+
110
+ Use browser_sitemap_query to read any page. Pass refresh=true to recrawl.`
111
+ );
112
+ return;
113
+ }
114
+ }
115
+ let sitemap;
116
+ if (depth > 0) {
117
+ sitemap = await (0, import_crawler.crawl)(url, {
118
+ depth,
119
+ maxPages,
120
+ sameOriginOnly,
121
+ probeButtons: probeMode,
122
+ playwrightStoragePath
123
+ });
124
+ } else {
125
+ sitemap = await (0, import_crawler.crawlSinglePage)(url, { playwrightStoragePath });
126
+ }
127
+ context.sitemapCache.set(key, { sitemap, storageStateHash, probeButtonsMode: probeMode });
128
+ const pageCount = Object.keys(sitemap.pages).length;
129
+ const pageList = Object.keys(sitemap.pages).map((u) => {
130
+ const bp = sitemap.pages[u];
131
+ const elCount = bp.sections.reduce((sum, s) => sum + s.elements.length + s.repeatingElements.length, 0);
132
+ return ` - ${u} (${elCount} elements across ${bp.sections.length} sections)`;
133
+ }).join("\n");
134
+ response.addTextResult(
135
+ `Sitemap built for ${sitemap.entryUrl}
136
+ Pages: ${pageCount}
137
+ Edges: ${sitemap.edges.length}
138
+ Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
139
+ Probe mode: ${probeMode}
140
+
141
+ Pages:
142
+ ${pageList}
143
+
144
+ Use browser_sitemap_query to read page blueprints, edges, or derived views (mapJson, outline).`
145
+ );
146
+ }
147
+ });
148
+ const sitemapQuery = (0, import_tool.defineTool)({
149
+ capability: "core",
150
+ schema: {
151
+ name: "browser_sitemap_query",
152
+ title: "Query the cached Sitemap",
153
+ description: [
154
+ "Query the Sitemap cached by browser_sitemap_build. Use this instead of re-calling sitemap_build.",
155
+ "",
156
+ "Modes:",
157
+ " page \u2014 returns the full canonical PageBlueprint for the given URL",
158
+ " (sections with enrichment, logical names, XPaths)",
159
+ " edges \u2014 returns navigation edges originating from the given URL",
160
+ " mapJson \u2014 returns the derived flat logicalName \u2192 xpath map for the URL",
161
+ " outline \u2014 returns the derived textual section-to-element hierarchy for the URL",
162
+ " overview \u2014 (when url is omitted) returns page list + edge list summary"
163
+ ].join("\n"),
164
+ inputSchema: import_mcpBundle.z.object({
165
+ type: import_mcpBundle.z.enum(["page", "edges", "mapJson", "outline"]).optional().describe("Query mode. Omit with url to get overview."),
166
+ url: import_mcpBundle.z.string().optional().describe("Page URL to query. Omit to get overview across all pages.")
167
+ }),
168
+ type: "readOnly"
169
+ },
170
+ handle: async (context, params, response) => {
171
+ if (context.sitemapCache.size === 0) {
172
+ response.addError("No Sitemap available. Run browser_sitemap_build first.");
173
+ return;
174
+ }
175
+ const latest = [...context.sitemapCache.values()].sort(
176
+ (a, b) => new Date(b.sitemap.cachedAt).getTime() - new Date(a.sitemap.cachedAt).getTime()
177
+ )[0];
178
+ const sitemap = latest.sitemap;
179
+ const { type, url } = params;
180
+ if (!url || !type) {
181
+ const overview = {
182
+ schemaVersion: sitemap.schemaVersion,
183
+ entryUrl: sitemap.entryUrl,
184
+ cachedAt: sitemap.cachedAt,
185
+ pages: Object.keys(sitemap.pages).map((u) => ({
186
+ url: u,
187
+ sections: sitemap.pages[u].sections.length,
188
+ elements: sitemap.pages[u].sections.reduce(
189
+ (s, sec) => s + sec.elements.length + sec.repeatingElements.length,
190
+ 0
191
+ ),
192
+ pageHash: sitemap.pages[u].pageHash
193
+ })),
194
+ edges: sitemap.edges
195
+ };
196
+ response.addTextResult(JSON.stringify(overview, null, 2));
197
+ return;
198
+ }
199
+ const normalizedQueryUrl = (0, import_serialization.normalizeUrl)(url);
200
+ const blueprint2 = sitemap.pages[normalizedQueryUrl];
201
+ if (!blueprint2) {
202
+ response.addError(
203
+ `Page not found in Sitemap: ${normalizedQueryUrl}. Available pages: ${Object.keys(sitemap.pages).join(", ")}`
204
+ );
205
+ return;
206
+ }
207
+ if (type === "page") {
208
+ response.addTextResult(JSON.stringify(blueprint2, null, 2));
209
+ return;
210
+ }
211
+ if (type === "edges") {
212
+ const edges = sitemap.edges.filter((e) => e.from === normalizedQueryUrl);
213
+ response.addTextResult(JSON.stringify({ url: normalizedQueryUrl, edges }, null, 2));
214
+ return;
215
+ }
216
+ if (type === "mapJson") {
217
+ response.addTextResult(JSON.stringify((0, import_blueprint.buildMap)(blueprint2), null, 2));
218
+ return;
219
+ }
220
+ if (type === "outline") {
221
+ response.addTextResult((0, import_blueprint.buildOutline)(blueprint2));
222
+ return;
223
+ }
224
+ }
225
+ });
226
+ const blueprint = (0, import_tool.defineTabTool)({
227
+ capability: "core",
228
+ schema: {
229
+ name: "browser_blueprint",
230
+ title: "Build PageBlueprint for the current page",
231
+ description: [
232
+ "Build a PageBlueprint for the currently loaded page. Returns the canonical `sections` tree",
233
+ "containing singular elements and repeating-element shapes, each carrying enrichment fields",
234
+ "(mutability, widgetType, framePath, shadowRoot, stableId, testId).",
235
+ "",
236
+ "Call this only when the DOM has changed since the last known-good blueprint \u2014 e.g. after a",
237
+ "modal opens, a form submits, a filter changes a list, or any mutable action. For pure navigation",
238
+ "to an already-crawled URL, reuse the Sitemap cache via browser_sitemap_query.",
239
+ "",
240
+ "Logical names are stable \u2014 use them in generated test code. Refs (from browser_snapshot) are",
241
+ "ephemeral \u2014 use them only for dispatching the next interaction, never in generated code."
242
+ ].join("\n"),
243
+ inputSchema: import_mcpBundle.z.object({}),
244
+ type: "readOnly"
245
+ },
246
+ handle: async (tab, _params, response) => {
247
+ try {
248
+ const bp = await (0, import_blueprint.buildPageBlueprint)(tab.page);
249
+ response.addTextResult(JSON.stringify(bp, null, 2));
250
+ } catch (err) {
251
+ if (err instanceof import_blueprint.BlueprintInvariantError) {
252
+ response.addError(
253
+ `Blueprint invariant violated: ${err.message}. This is a builder bug; please file an issue with the URL of the page that triggered it.`
254
+ );
255
+ return;
256
+ }
257
+ throw err;
258
+ }
259
+ }
260
+ });
261
+ var domAnalyzer_default = [sitemapBuild, sitemapQuery, blueprint];
@@ -29,9 +29,9 @@ const pressKey = (0, import_tool.defineTabTool)({
29
29
  schema: {
30
30
  name: "browser_press_key",
31
31
  title: "Press a key",
32
- description: "Press a key on the keyboard",
32
+ description: "Press a single key on the globally-focused element. This is a FALLBACK tool \u2014 prefer higher-level tools whenever possible: browser_click for buttons and links, browser_type for text entry, browser_type with submit: true for form submission (bundles the fill and the Enter into one recorded action). Only use browser_press_key when no semantic alternative exists: arrow-key navigation in an already-open listbox or combobox, Escape to close a modal that has no visible close button, Tab to shift focus between fields, or an app-specific keyboard shortcut the user actually relies on (e.g. Ctrl+K to open a command palette).",
33
33
  inputSchema: import_mcpBundle.z.object({
34
- key: import_mcpBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
34
+ key: import_mcpBundle.z.string().describe('Key name or character, e.g. "ArrowDown", "Enter", "Escape", "a". Supports modifier combinations like "Control+a" or "Meta+v".')
35
35
  }),
36
36
  type: "input"
37
37
  },
@@ -77,7 +77,7 @@ const type = (0, import_tool.defineTabTool)({
77
77
  schema: {
78
78
  name: "browser_type",
79
79
  title: "Type text",
80
- description: "Type text into an editable element. Auto-focuses and replaces existing content. NEVER call browser_click on the field first \u2014 clicking before typing injects extra network requests that corrupt the trace.",
80
+ description: "Type text into an editable element. Auto-focuses and replaces existing content. NEVER call browser_click on the field first \u2014 clicking before typing injects extra network requests that corrupt the trace. When recording a UI trace for capture-act-capture, capture browser_blueprint before and after to derive form-state assertions from the textChanges delta.",
81
81
  inputSchema: typeSchema,
82
82
  type: "input"
83
83
  },