@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.
- package/build/index.js +4 -2
- package/build/playwright/registerPlaywrightTools.js +12 -0
- package/build/playwright/traceRecordingPrompt.js +15 -0
- package/build/prompts/code-reuse.js +106 -7
- package/build/prompts/pom-aware-code-reuse.js +106 -7
- package/build/prompts/startTraceCollectionPrompts.js +37 -15
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
- package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
- package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
- package/build/prompts/test-recommendation/promptPlan.js +290 -0
- package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
- package/build/prompts/test-recommendation/recommendationSections.js +4 -3
- package/build/prompts/test-recommendation/recommendationShared.js +23 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
- package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
- package/build/prompts/testbot/testbot-prompts.js +73 -13
- package/build/prompts/testbot/testbot-prompts.test.js +114 -1
- package/build/resources/testbotResource.js +1 -1
- package/build/services/ScenarioGenerationService.integration.test.js +158 -0
- package/build/services/ScenarioGenerationService.js +47 -4
- package/build/services/ScenarioGenerationService.test.js +158 -22
- package/build/services/TestExecutionService.js +73 -15
- package/build/services/TestExecutionService.test.js +105 -0
- package/build/services/TestGenerationService.js +11 -1
- package/build/tools/executeSkyrampTestTool.js +1 -10
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +2 -0
- package/build/tools/test-management/actionsTool.js +152 -63
- package/build/tools/test-management/analyzeChangesTool.js +178 -64
- package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
- package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
- package/build/tools/trace/resolveSaveStoragePath.js +16 -0
- package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
- package/build/tools/trace/resolveSessionPaths.js +39 -0
- package/build/tools/trace/resolveSessionPaths.test.js +103 -0
- package/build/tools/trace/sessionState.js +14 -0
- package/build/tools/trace/sessionState.test.js +17 -0
- package/build/tools/trace/startTraceCollectionTool.js +84 -14
- package/build/tools/trace/stopTraceCollectionTool.js +9 -2
- package/build/types/TestAnalysis.js +50 -0
- package/build/types/TestRecommendation.js +6 -58
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.js +22 -11
- package/build/utils/branchDiff.js +11 -2
- package/build/utils/docker.test.js +1 -1
- package/build/utils/gitStaging.js +52 -3
- package/build/utils/gitStaging.test.js +19 -1
- package/build/utils/repoScanner.js +18 -10
- package/build/utils/repoScanner.test.js +92 -0
- package/build/utils/routeParsers.js +180 -25
- package/build/utils/routeParsers.test.js +180 -1
- package/build/utils/scenarioDrafting.js +220 -17
- package/build/utils/scenarioDrafting.test.js +182 -9
- package/build/utils/sourceRouteExtractor.js +806 -0
- package/build/utils/sourceRouteExtractor.test.js +565 -0
- package/build/utils/uiPageEnumerator.js +319 -0
- package/build/utils/uiPageEnumerator.test.js +422 -0
- package/build/utils/utils.js +27 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.js +33 -4
- package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
- package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
- package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
- package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
- package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
- package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
- package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
- package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
- package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
- package/node_modules/playwright/package.json +1 -1
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
- package/package.json +3 -3
- package/build/services/TestHealthService.js +0 -694
- package/build/services/TestHealthService.test.js +0 -241
- package/build/types/TestDriftAnalysis.js +0 -1
- 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
|
|
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:
|
|
73
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
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++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
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
|
},
|