@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,168 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var widgetContract_exports = {};
20
+ __export(widgetContract_exports, {
21
+ default: () => widgetContract_default
22
+ });
23
+ module.exports = __toCommonJS(widgetContract_exports);
24
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
25
+ var import_tool = require("./tool");
26
+ var import_widgetContract = require("../../../dom-analyzer/widgetContract");
27
+ const widgetContractLookup = (0, import_tool.defineTabTool)({
28
+ capability: "core",
29
+ schema: {
30
+ name: "browser_widget_contract_lookup",
31
+ title: "Look up a widget interaction contract by fingerprint",
32
+ description: [
33
+ "Returns the interaction contract for a widget's fingerprint.",
34
+ "",
35
+ "Only call for elements whose `widgetType` is `custom` or `unknown` \u2014 native widgets don't",
36
+ "need contracts. Pass the element's ref along with the fingerprint.",
37
+ "",
38
+ 'On `status: "needs_inference"`, synthesize a WidgetContract from your own reasoning using',
39
+ "the returned context (outerHtml, ancestor chain, aria subtree, optional portal candidate),",
40
+ "submit it via `browser_widget_contract_cache`, then execute the contract using existing",
41
+ "browser tools (`browser_click`, `browser_type`, etc.).",
42
+ "",
43
+ 'On `status: "error", reason: "stale_ref"`, re-snapshot and retry.',
44
+ "",
45
+ 'On `status: "error", reason: "context_gather_failed"`, inspect the message; the error may be transient (retry) or structural (give up and try a different ref).',
46
+ "",
47
+ "Do not speculatively pre-infer contracts \u2014 call only when about to interact."
48
+ ].join("\n"),
49
+ inputSchema: import_mcpBundle.z.object({
50
+ fingerprint: import_mcpBundle.z.string().regex(/^[0-9a-f]{16}$/).describe("16-hex fingerprint from the element's PageBlueprint entry"),
51
+ ref: import_mcpBundle.z.string().describe('Snapshot ref for the specific element instance (e.g. "e42")')
52
+ }),
53
+ type: "readOnly"
54
+ },
55
+ handle: async (tab, params, response) => {
56
+ const { fingerprint, ref } = params;
57
+ const pure = (0, import_widgetContract.lookupContract)(fingerprint, tab.context.inferredContracts);
58
+ if (pure.status === "found") {
59
+ response.addTextResult(JSON.stringify({ status: "found", contract: pure.contract }, null, 2));
60
+ return;
61
+ }
62
+ let locator;
63
+ try {
64
+ ({ locator } = await tab.refLocator({ ref, element: "widget-fingerprint-target" }));
65
+ } catch {
66
+ response.addTextResult(JSON.stringify({ status: "error", reason: "stale_ref" }, null, 2));
67
+ return;
68
+ }
69
+ let gather;
70
+ let ariaRaw;
71
+ try {
72
+ gather = await locator.evaluate((el) => {
73
+ function truncate(s, max) {
74
+ return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
75
+ }
76
+ const outerHtml = truncate(el.outerHTML, 2048);
77
+ const ancestorChain = [];
78
+ let cur = el.parentElement;
79
+ let depth = 0;
80
+ while (cur && depth < 8) {
81
+ const role = cur.getAttribute("role");
82
+ const tag = cur.tagName.toLowerCase();
83
+ ancestorChain.push(role ? `${tag}[role='${role}']` : tag);
84
+ if (tag === "body") break;
85
+ cur = cur.parentElement;
86
+ depth++;
87
+ }
88
+ let portalEl = null;
89
+ const ctrl = el.getAttribute("aria-controls");
90
+ if (ctrl) {
91
+ const t = document.getElementById(ctrl);
92
+ if (t && !el.contains(t)) portalEl = t;
93
+ }
94
+ if (!portalEl) {
95
+ let found = 0;
96
+ const walker = document.createTreeWalker(document.documentElement, NodeFilter.SHOW_ELEMENT);
97
+ let cur2;
98
+ while (cur2 = walker.nextNode()) {
99
+ const desc = cur2;
100
+ if (el.contains(desc)) continue;
101
+ for (const attr of Array.from(desc.attributes)) {
102
+ if (attr.name.startsWith("data-radix-") || attr.name.startsWith("data-headlessui-")) {
103
+ portalEl = desc;
104
+ break;
105
+ }
106
+ }
107
+ if (portalEl) break;
108
+ if (++found >= 20) break;
109
+ }
110
+ }
111
+ const portal = portalEl ? {
112
+ selector: portalEl.tagName.toLowerCase() + (portalEl.id ? `#${portalEl.id}` : ""),
113
+ outerHtml: truncate(portalEl.outerHTML, 2048)
114
+ } : void 0;
115
+ return { outerHtml, ancestorChain, portal };
116
+ });
117
+ ariaRaw = await locator.ariaSnapshot();
118
+ } catch (e) {
119
+ response.addTextResult(JSON.stringify({
120
+ status: "error",
121
+ reason: "context_gather_failed",
122
+ message: e.message
123
+ }, null, 2));
124
+ return;
125
+ }
126
+ const ariaSnapshot = ariaRaw.length <= 1024 ? ariaRaw : ariaRaw.slice(0, 1023) + "\u2026";
127
+ const result = {
128
+ status: "needs_inference",
129
+ fingerprint,
130
+ ref,
131
+ element: { outerHtml: gather.outerHtml, ancestorChain: gather.ancestorChain },
132
+ ariaSnapshot,
133
+ ...gather.portal ? { portalCandidate: gather.portal } : {}
134
+ };
135
+ response.addTextResult(JSON.stringify(result, null, 2));
136
+ }
137
+ });
138
+ const widgetContractCache = (0, import_tool.defineTabTool)({
139
+ capability: "core",
140
+ schema: {
141
+ name: "browser_widget_contract_cache",
142
+ title: "Cache an inferred widget contract",
143
+ description: [
144
+ "Writes an inferred widget contract into the session's cache.",
145
+ "",
146
+ "Refuses to overwrite curated entries. Mutable for inferred entries \u2014 resubmit to",
147
+ 'correct a bad inference. Submit a contract with `source: "unknown"` and empty steps',
148
+ "to mark the fingerprint as unresolvable for the session.",
149
+ "",
150
+ "Execute the contract yourself after a successful write; the tool does not return it back."
151
+ ].join("\n"),
152
+ inputSchema: import_mcpBundle.z.object({
153
+ fingerprint: import_mcpBundle.z.string().regex(/^[0-9a-f]{16}$/).describe("16-hex fingerprint of the widget being cached \u2014 must match the fingerprint that browser_widget_contract_lookup returned in needs_inference status (or that you read from a PageBlueprint element's fingerprint field)."),
154
+ // InferredWidgetContractSchema (not WidgetContractSchema): narrows
155
+ // `source` to {'inferred','unknown'}. Curated contracts come only
156
+ // from the curated library — agents must not be able to mislabel an
157
+ // inferred entry as authoritative via this tool surface.
158
+ contract: import_widgetContract.InferredWidgetContractSchema.describe(`The widget's interaction contract. Set source to "inferred" for a real contract you authored, or "unknown" with empty steps to mark the widget as unresolvable for this session. Curated contracts come from the library \u2014 you cannot submit source: "curated".`)
159
+ }),
160
+ type: "readOnly"
161
+ },
162
+ handle: async (tab, params, response) => {
163
+ const { fingerprint, contract } = params;
164
+ const result = (0, import_widgetContract.cacheInferredContract)(fingerprint, contract, tab.context.inferredContracts);
165
+ response.addTextResult(JSON.stringify(result));
166
+ }
167
+ });
168
+ var widgetContract_default = [widgetContractLookup, widgetContractCache];
@@ -44,16 +44,22 @@ var import_mouse = __toESM(require("./tools/mouse"));
44
44
  var import_navigate = __toESM(require("./tools/navigate"));
45
45
  var import_network = __toESM(require("./tools/network"));
46
46
  var import_open = __toESM(require("./tools/open"));
47
+ var import_pageBlueprint = __toESM(require("./tools/pageBlueprint"));
47
48
  var import_pdf = __toESM(require("./tools/pdf"));
48
49
  var import_runCode = __toESM(require("./tools/runCode"));
50
+ var import_sitemap = __toESM(require("./tools/sitemap"));
49
51
  var import_snapshot = __toESM(require("./tools/snapshot"));
50
52
  var import_screenshot = __toESM(require("./tools/screenshot"));
51
53
  var import_tabs = __toESM(require("./tools/tabs"));
52
54
  var import_tracing = __toESM(require("./tools/tracing"));
53
55
  var import_wait = __toESM(require("./tools/wait"));
54
56
  var import_verify = __toESM(require("./tools/verify"));
57
+ var import_widgetContract = __toESM(require("./tools/widgetContract"));
55
58
  const browserTools = [
56
59
  ...import_common.default,
60
+ ...import_sitemap.default,
61
+ ...import_pageBlueprint.default,
62
+ ...import_widgetContract.default,
57
63
  ...import_console.default,
58
64
  ...import_dialogs.default,
59
65
  ...import_evaluate.default,
@@ -66,11 +66,22 @@ class TraceRecordingBackend {
66
66
  return this._trackedActions;
67
67
  }
68
68
  async initialize(clientInfo) {
69
- const headless = this._options.headless ?? (!!process.env.CI || import_os.default.platform() === "linux" && !process.env.DISPLAY);
69
+ const userDataDir = this._options.userDataDir || process.env.PLAYWRIGHT_USER_DATA_DIR || void 0;
70
+ const loadExtension = this._options.loadExtension ?? splitExtensionPaths(process.env.PLAYWRIGHT_LOAD_EXTENSION);
71
+ const persistent = !!userDataDir || !!loadExtension?.length;
72
+ const extensionArgs = loadExtension?.length ? [
73
+ `--disable-extensions-except=${loadExtension.join(",")}`,
74
+ `--load-extension=${loadExtension.join(",")}`
75
+ ] : void 0;
76
+ const headless = loadExtension?.length ? false : this._options.headless ?? (!!process.env.CI || import_os.default.platform() === "linux" && !process.env.DISPLAY);
70
77
  const config = await (0, import_config.resolveConfig)({
71
78
  browser: {
72
- isolated: true,
73
- launchOptions: { headless },
79
+ isolated: !persistent,
80
+ ...persistent && userDataDir ? { userDataDir } : {},
81
+ launchOptions: {
82
+ headless,
83
+ ...extensionArgs ? { args: extensionArgs } : {}
84
+ },
74
85
  contextOptions: {
75
86
  viewport: { width: 1280, height: 900 },
76
87
  recordHar: { path: this._harPath, mode: "minimal" },
@@ -88,11 +99,18 @@ class TraceRecordingBackend {
88
99
  navigation: 6e4
89
100
  }
90
101
  });
102
+ if (loadExtension?.length && config.browser.launchOptions?.channel === "chrome")
103
+ delete config.browser.launchOptions.channel;
91
104
  const factory = (0, import_browserContextFactory.contextFactory)(config);
92
105
  this._browserBackend = new import_browserServerBackend.BrowserServerBackend(config, factory);
93
106
  await this._browserBackend.initialize(clientInfo);
94
107
  this._initialized = true;
95
- traceDebug("TraceRecordingBackend initialized");
108
+ traceDebug("TraceRecordingBackend initialized", {
109
+ persistent,
110
+ userDataDir,
111
+ loadExtension,
112
+ channel: config.browser.launchOptions?.channel
113
+ });
96
114
  this._browserBackend.context.onBrowserContextCreated = (browserContext) => this._installPopupListener(browserContext);
97
115
  }
98
116
  async listTools() {
@@ -130,8 +148,10 @@ class TraceRecordingBackend {
130
148
  }
131
149
  if (name === "browser_tabs" && ["select", "switch"].includes(args?.action)) {
132
150
  const index = args?.index ?? 0;
133
- this._currentPageAlias = index === 0 ? "page" : `page${index}`;
134
- traceDebug(`Tab switched to index ${index} \u2192 pageAlias: ${this._currentPageAlias}`);
151
+ const targetUrl = this._browserBackend.context?.tabs()[index]?.page.url();
152
+ const urlAlias = targetUrl ? aliasForUrl(targetUrl) : null;
153
+ this._currentPageAlias = urlAlias ?? (index === 0 ? "page" : `page${index}`);
154
+ traceDebug(`Tab switched to index ${index} (url=${targetUrl}) \u2192 pageAlias: ${this._currentPageAlias}`);
135
155
  const tabResult = await this._browserBackend.callTool(name, args);
136
156
  return tabResult;
137
157
  }
@@ -731,7 +751,7 @@ ${details}` }]
731
751
  */
732
752
  _installPopupListener(browserContext) {
733
753
  let initialPageSeen = false;
734
- browserContext.on("page", () => {
754
+ browserContext.on("page", (newPage) => {
735
755
  if (!initialPageSeen) {
736
756
  initialPageSeen = true;
737
757
  return;
@@ -745,16 +765,23 @@ ${details}` }]
745
765
  this._currentPageAlias = popupAlias;
746
766
  this._pendingPopupAlias = popupAlias;
747
767
  traceDebug(`Popup page opened: ${popupAlias} (pending stamp)`);
768
+ void newPage.waitForLoadState("domcontentloaded").then(() => {
769
+ const settled = newPage.url();
770
+ const semantic = aliasForUrl(settled);
771
+ if (!semantic)
772
+ return;
773
+ if (this._currentPageAlias === popupAlias)
774
+ this._currentPageAlias = semantic;
775
+ if (this._pendingPopupAlias === popupAlias)
776
+ this._pendingPopupAlias = semantic;
777
+ traceDebug(`Popup alias upgraded: ${popupAlias} \u2192 ${semantic} (url=${settled})`);
778
+ }).catch(() => {
779
+ });
748
780
  });
749
781
  }
750
782
  _maybeTrackAction(toolName, args, result, timestamp, pageAliasBeforeAction) {
751
783
  if (result.isError)
752
784
  return;
753
- if (toolName === "browser_press_key") {
754
- const key = String(args.key || "").toLowerCase();
755
- if (key.includes("control+") || key.includes("meta+") || key === "tab" || key === "enter" || key === "escape")
756
- return;
757
- }
758
785
  const parsed = (0, import_response.parseResponse)(result);
759
786
  const code = parsed?.code ?? "";
760
787
  if (code || import_types.ARGS_ONLY_TOOLS.has(toolName)) {
@@ -811,6 +838,19 @@ ${details}` }]
811
838
  this._cleanupTempDir(false);
812
839
  }
813
840
  }
841
+ function splitExtensionPaths(value) {
842
+ if (!value)
843
+ return void 0;
844
+ const parts = value.split(/[,;]/).map((p) => p.trim()).filter(Boolean);
845
+ return parts.length ? parts : void 0;
846
+ }
847
+ function aliasForUrl(url) {
848
+ if (!url)
849
+ return null;
850
+ if (/^chrome-extension:\/\/[a-p]{32}\/popup\//.test(url))
851
+ return "popupPage";
852
+ return null;
853
+ }
814
854
  // Annotate the CommonJS export names for ESM import in node:
815
855
  0 && (module.exports = {
816
856
  TraceRecordingBackend
@@ -66,6 +66,16 @@ function jsonlHeader(browserName, harPath) {
66
66
  });
67
67
  }
68
68
  function extractLocatorFromCode(code) {
69
+ const extensionFrameMatch = code.match(
70
+ /await\s+page\.frames\(\)\.find\(\s*f\s*=>\s*f\.url\(\)\.includes\(\s*(['"])([^'"]+)\1\s*\)\s*\)!?\.(.*?)\.(click|dblclick|fill|pressSequentially|check|uncheck|selectOption|hover|dragTo)\s*\(/s
71
+ );
72
+ if (extensionFrameMatch) {
73
+ return {
74
+ locatorExpr: extensionFrameMatch[3].trim(),
75
+ framePath: [],
76
+ frameUrlMatch: extensionFrameMatch[2]
77
+ };
78
+ }
69
79
  const contentFrameMatch = code.match(/await\s+page\.locator\((['"])(iframe[^\n]*?)\1\)(?:\.contentFrame\(\))+\.(.*?)\.(click|dblclick|fill|pressSequentially|check|uncheck|selectOption|hover|dragTo)\s*\(/s);
70
80
  if (contentFrameMatch) {
71
81
  return { locatorExpr: contentFrameMatch[3].trim(), framePath: [contentFrameMatch[2]] };
@@ -206,10 +216,13 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
206
216
  if (toolName === "browser_wait_for") {
207
217
  const text = args.text;
208
218
  const textGone = args.textGone;
219
+ const time = args.time;
209
220
  if (text)
210
221
  return JSON.stringify({ name: "waitForSelector", selector: `text=${text}`, ...base });
211
222
  if (textGone)
212
223
  return JSON.stringify({ name: "waitForSelector", selector: `text=${textGone}`, state: "hidden", ...base });
224
+ if (typeof time === "number")
225
+ return JSON.stringify({ name: "waitForTimeout", duration: time * 1e3, ...base });
213
226
  return null;
214
227
  }
215
228
  if (toolName === "browser_press_key")
@@ -219,9 +232,11 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
219
232
  const extracted = extractLocatorFromCode(code);
220
233
  if (!extracted)
221
234
  return null;
222
- const { locatorExpr, framePath: codeFramePath } = extracted;
235
+ const { locatorExpr, framePath: codeFramePath, frameUrlMatch } = extracted;
223
236
  if (codeFramePath.length > 0)
224
237
  base.framePath = codeFramePath;
238
+ if (frameUrlMatch)
239
+ base.frameUrlMatch = frameUrlMatch;
225
240
  const parsed = locatorToJsonl(locatorExpr);
226
241
  if (!parsed)
227
242
  return null;
@@ -373,18 +388,51 @@ function removeRedundantClicks(actions) {
373
388
  });
374
389
  }
375
390
  function deduplicateRetries(actions) {
376
- const firstNavigate = actions.find((a) => a.toolName === "browser_navigate");
377
- if (!firstNavigate)
378
- return actions;
379
- const startUrl = firstNavigate.args.url;
380
- let lastRestartIdx = 0;
381
- for (let i = 1; i < actions.length; i++) {
382
- if (actions[i].toolName === "browser_navigate" && actions[i].args.url === startUrl)
383
- lastRestartIdx = i;
391
+ const result = [];
392
+ let lastNavIdx = -1;
393
+ for (const action of actions) {
394
+ if (action.toolName !== "browser_navigate") {
395
+ result.push(action);
396
+ continue;
397
+ }
398
+ const url = action.args.url;
399
+ if (lastNavIdx >= 0 && result[lastNavIdx].args.url === url) {
400
+ result.length = lastNavIdx;
401
+ lastNavIdx = -1;
402
+ for (let k = result.length - 1; k >= 0; k--) {
403
+ if (result[k].toolName === "browser_navigate") {
404
+ lastNavIdx = k;
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ result.push(action);
410
+ lastNavIdx = result.length - 1;
411
+ }
412
+ return result;
413
+ }
414
+ function normalizePrimaryPageAlias(actions) {
415
+ const DEFAULT = DEFAULT_PAGE_ALIAS;
416
+ const counts = /* @__PURE__ */ new Map();
417
+ for (const a of actions) {
418
+ const alias = a.pageAlias ?? DEFAULT;
419
+ counts.set(alias, (counts.get(alias) ?? 0) + 1);
420
+ }
421
+ if ((counts.get(DEFAULT) ?? 0) > 0)
422
+ return;
423
+ const candidates = [...counts.keys()].filter((a) => /^page\d+$/.test(a));
424
+ if (candidates.length !== 1)
425
+ return;
426
+ const from = candidates[0];
427
+ for (const a of actions) {
428
+ if (a.pageAlias === from)
429
+ a.pageAlias = DEFAULT;
430
+ if (a.popupAlias === from)
431
+ a.popupAlias = DEFAULT;
384
432
  }
385
- return lastRestartIdx > 0 ? actions.slice(lastRestartIdx) : actions;
386
433
  }
387
434
  function buildJsonlContent(actions, browserName, harPath) {
435
+ normalizePrimaryPageAlias(actions);
388
436
  const deduplicated = removeRedundantClicks(deduplicateRetries(actions));
389
437
  const startTime = deduplicated[0]?.timestamp ?? Date.now();
390
438
  const pageGuids = /* @__PURE__ */ new Map();
@@ -416,7 +464,9 @@ function buildJsonlContent(actions, browserName, harPath) {
416
464
  const obj = JSON.parse(lines[i]);
417
465
  if (obj.name === "openPage" || obj.name === "closePage")
418
466
  continue;
419
- if (!obj.pageAlias || obj.pageAlias !== alias) {
467
+ if (!obj.pageAlias)
468
+ continue;
469
+ if (obj.pageAlias !== alias) {
420
470
  obj.signals = [...obj.signals || [], { name: "popup", popupAlias: alias }];
421
471
  lines[i] = JSON.stringify(obj);
422
472
  break;
@@ -453,7 +503,7 @@ function buildJsonlContent(actions, browserName, harPath) {
453
503
  }
454
504
  const parsed = extractLocatorFromCode(action.code);
455
505
  if (parsed) {
456
- const { locatorExpr, framePath } = parsed;
506
+ const { locatorExpr, framePath, frameUrlMatch } = parsed;
457
507
  const locatorInfo = locatorToJsonl(locatorExpr);
458
508
  if (locatorInfo) {
459
509
  const { selector, locator: locatorObj } = locatorInfo;
@@ -466,7 +516,8 @@ function buildJsonlContent(actions, browserName, harPath) {
466
516
  timestamp: String(action.timestamp + 100),
467
517
  pageGuid,
468
518
  pageAlias: alias,
469
- framePath
519
+ framePath,
520
+ ...frameUrlMatch ? { frameUrlMatch } : {}
470
521
  });
471
522
  lines.push(pressLine);
472
523
  actionCount++;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/playwright",
3
- "version": "1.58.2-skyramp.8.9.0",
3
+ "version": "1.58.2-skyramp.8.9.6",
4
4
  "description": "Skyramp's fork of Playwright with trace recording for UI test generation",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.1.8",
3
+ "version": "0.2.0-rc.2",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",
@@ -55,11 +55,11 @@
55
55
  "dependencies": {
56
56
  "@modelcontextprotocol/sdk": "^1.24.3",
57
57
  "@playwright/test": "^1.55.0",
58
- "@skyramp/skyramp": "1.3.24",
58
+ "@skyramp/skyramp": "1.3.25",
59
59
  "dockerode": "^5.0.0",
60
60
  "fast-glob": "^3.3.3",
61
61
  "js-yaml": "^4.1.1",
62
- "playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.9.0.tgz",
62
+ "playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz",
63
63
  "simple-git": "^3.30.0",
64
64
  "zod": "^3.25.3"
65
65
  },