@trusty-squire/mcp 0.8.2-rc.8 → 0.8.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/dist/bot/agent.d.ts +8 -0
- package/dist/bot/agent.d.ts.map +1 -1
- package/dist/bot/agent.js +648 -142
- package/dist/bot/agent.js.map +1 -1
- package/dist/bot/browser.d.ts +2 -0
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +247 -12
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/promote-to-skill.d.ts.map +1 -1
- package/dist/bot/promote-to-skill.js +50 -2
- package/dist/bot/promote-to-skill.js.map +1 -1
- package/dist/bot/replay-skill.d.ts +15 -0
- package/dist/bot/replay-skill.d.ts.map +1 -1
- package/dist/bot/replay-skill.js +354 -25
- package/dist/bot/replay-skill.js.map +1 -1
- package/dist/install/interactive.d.ts.map +1 -1
- package/dist/install/interactive.js +23 -9
- package/dist/install/interactive.js.map +1 -1
- package/package.json +1 -1
package/dist/bot/browser.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export declare class BrowserController {
|
|
|
63
63
|
}>;
|
|
64
64
|
selectOption(selector: string, optionMatcher?: string): Promise<void>;
|
|
65
65
|
private selectFromCombobox;
|
|
66
|
+
private resolveLabelToInput;
|
|
67
|
+
private tryReactSelectKeyboardPick;
|
|
66
68
|
private pickComboboxOption;
|
|
67
69
|
private humanClick;
|
|
68
70
|
private humanClickLocator;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/bot/browser.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/bot/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/bot/browser.js
CHANGED
|
@@ -962,10 +962,34 @@ export class BrowserController {
|
|
|
962
962
|
if (!this.page)
|
|
963
963
|
throw new Error("Browser not started");
|
|
964
964
|
await this.page.waitForSelector(selector, { state: "attached", timeout: 10000 });
|
|
965
|
-
|
|
966
|
-
|
|
965
|
+
let activeSelector = selector;
|
|
966
|
+
let tagName = await this.page
|
|
967
|
+
.locator(activeSelector)
|
|
967
968
|
.first()
|
|
968
969
|
.evaluate((node) => node.tagName.toLowerCase());
|
|
970
|
+
// 0.8.2-rc.21 — Railway-class fix. The captured selector frequently
|
|
971
|
+
// points at a `<label>` (the inventory ranker prefers visible-text
|
|
972
|
+
// elements). If that label's `for=` association resolves to a
|
|
973
|
+
// native `<select>`, take the native path instead of routing into
|
|
974
|
+
// selectFromCombobox — native selects don't reveal their options
|
|
975
|
+
// via any DOM pattern in headless Chromium (they're OS-rendered),
|
|
976
|
+
// so the combobox path is guaranteed to fail for them. Without
|
|
977
|
+
// this redirect, every captured Railway/legacy-form `<select>`
|
|
978
|
+
// step replays as "no options found after click."
|
|
979
|
+
if (tagName === "label") {
|
|
980
|
+
const resolved = await this.resolveLabelToInput(activeSelector);
|
|
981
|
+
if (resolved !== activeSelector) {
|
|
982
|
+
const resolvedTag = await this.page
|
|
983
|
+
.locator(resolved)
|
|
984
|
+
.first()
|
|
985
|
+
.evaluate((node) => node.tagName.toLowerCase())
|
|
986
|
+
.catch(() => "");
|
|
987
|
+
if (resolvedTag === "select") {
|
|
988
|
+
activeSelector = resolved;
|
|
989
|
+
tagName = "select";
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
969
993
|
if (tagName === "select") {
|
|
970
994
|
// Native path. rc.15 — keep value="" options selectable. The
|
|
971
995
|
// Railway workspace dropdown's "No workspace" option is value=""
|
|
@@ -976,10 +1000,10 @@ export class BrowserController {
|
|
|
976
1000
|
// (with the first option's value, even if empty), and a matched
|
|
977
1001
|
// text-based pick is honored verbatim — including empty values.
|
|
978
1002
|
const allValues = await this.page
|
|
979
|
-
.locator(`${
|
|
1003
|
+
.locator(`${activeSelector} option`)
|
|
980
1004
|
.evaluateAll((opts) => opts.map((o) => (o instanceof HTMLOptionElement ? o.value : "")));
|
|
981
1005
|
if (allValues.length === 0) {
|
|
982
|
-
throw new Error(`<select> ${
|
|
1006
|
+
throw new Error(`<select> ${activeSelector} has no selectable option`);
|
|
983
1007
|
}
|
|
984
1008
|
// Default to the first NON-empty value when the planner gave no
|
|
985
1009
|
// hint — historic behavior, kept because "Select…" placeholder
|
|
@@ -992,7 +1016,7 @@ export class BrowserController {
|
|
|
992
1016
|
// option's text matches. Wrap in an object so we can
|
|
993
1017
|
// distinguish "matched to empty value" from "no match".
|
|
994
1018
|
const matched = await this.page
|
|
995
|
-
.locator(`${
|
|
1019
|
+
.locator(`${activeSelector} option`)
|
|
996
1020
|
.evaluateAll((opts, needle) => {
|
|
997
1021
|
const hit = opts
|
|
998
1022
|
.filter((o) => o instanceof HTMLOptionElement)
|
|
@@ -1004,9 +1028,9 @@ export class BrowserController {
|
|
|
1004
1028
|
}
|
|
1005
1029
|
}
|
|
1006
1030
|
if (chosenValue === undefined) {
|
|
1007
|
-
throw new Error(`<select> ${
|
|
1031
|
+
throw new Error(`<select> ${activeSelector} has no selectable option`);
|
|
1008
1032
|
}
|
|
1009
|
-
await this.page.selectOption(
|
|
1033
|
+
await this.page.selectOption(activeSelector, chosenValue);
|
|
1010
1034
|
// rc.17 — mark the element as touched so subsequent inventory
|
|
1011
1035
|
// reads can suppress the DEFAULTED-dropdown warning for it.
|
|
1012
1036
|
// Without this, a select whose committed value is "" (Railway's
|
|
@@ -1014,7 +1038,7 @@ export class BrowserController {
|
|
|
1014
1038
|
// the planner gets stuck in a select→select→… loop trying to
|
|
1015
1039
|
// satisfy a warning the form has already satisfied.
|
|
1016
1040
|
await this.page
|
|
1017
|
-
.locator(
|
|
1041
|
+
.locator(activeSelector)
|
|
1018
1042
|
.first()
|
|
1019
1043
|
.evaluate((el) => {
|
|
1020
1044
|
if (el instanceof HTMLElement)
|
|
@@ -1025,7 +1049,7 @@ export class BrowserController {
|
|
|
1025
1049
|
}
|
|
1026
1050
|
// Custom combobox path. Sentry, Radix, Headless UI, React Aria
|
|
1027
1051
|
// — every modern React picker emits role=option on its items.
|
|
1028
|
-
await this.selectFromCombobox(
|
|
1052
|
+
await this.selectFromCombobox(activeSelector, optionMatcher);
|
|
1029
1053
|
}
|
|
1030
1054
|
// F11 (+rc.7 hardening): click a combobox trigger, wait for the
|
|
1031
1055
|
// listbox to open, click an option.
|
|
@@ -1064,7 +1088,18 @@ export class BrowserController {
|
|
|
1064
1088
|
async selectFromCombobox(triggerSelector, optionMatcher) {
|
|
1065
1089
|
if (!this.page)
|
|
1066
1090
|
throw new Error("Browser not started");
|
|
1067
|
-
|
|
1091
|
+
// 0.8.2-rc.11 — selector normalization. The planner sometimes
|
|
1092
|
+
// emits a selector pointing at a `<label for="X">` instead of the
|
|
1093
|
+
// associated `<input id="X">` — the label has the visible text
|
|
1094
|
+
// ("Project") so the inventory ranking surfaces it as the target.
|
|
1095
|
+
// Clicking a label is NOT equivalent to clicking the input for
|
|
1096
|
+
// react-select: the synthetic focus DOES move to the input via
|
|
1097
|
+
// the `for` association, but no mouse-down lands on the
|
|
1098
|
+
// react-select control, so the menu never opens. Resolve the
|
|
1099
|
+
// label to its associated input here so downstream tiers (the
|
|
1100
|
+
// keyboard fallback in particular) actually see an input target.
|
|
1101
|
+
const normalizedSelector = await this.resolveLabelToInput(triggerSelector);
|
|
1102
|
+
await this.humanClick(normalizedSelector);
|
|
1068
1103
|
const patternSelectors = [
|
|
1069
1104
|
'[role="option"]:visible',
|
|
1070
1105
|
'[role="menuitem"]:visible',
|
|
@@ -1088,6 +1123,19 @@ export class BrowserController {
|
|
|
1088
1123
|
await this.pickComboboxOption(locator, optionMatcher);
|
|
1089
1124
|
return;
|
|
1090
1125
|
}
|
|
1126
|
+
// 0.8.2-rc.11 — keyboard-driven react-select fallback. Sentry's
|
|
1127
|
+
// permission-grid combobox (Project--permission, Team--permission,
|
|
1128
|
+
// …) is a react-select 5 instance: clicking the inner <input> only
|
|
1129
|
+
// focuses it; the menu opens on keyboard activity. The standard
|
|
1130
|
+
// pattern is: Alt+Down (or just type a character) to open + filter,
|
|
1131
|
+
// then Enter to commit. Try Alt+Down first so an instance with
|
|
1132
|
+
// visible options but no role="option" still works; then if a
|
|
1133
|
+
// matcher was given, type-to-filter + Enter so a hidden listbox
|
|
1134
|
+
// narrows directly to the right option.
|
|
1135
|
+
if (await this.tryReactSelectKeyboardPick(normalizedSelector, optionMatcher)) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
triedDescriptors.push("react-select keyboard (Alt+Down, type-to-filter, Enter)");
|
|
1091
1139
|
// ARIA tiers all empty. Text-based fallback, only if the planner
|
|
1092
1140
|
// told us WHICH option to pick — without a matcher, "first text
|
|
1093
1141
|
// on the page" would click unrelated UI.
|
|
@@ -1104,11 +1152,169 @@ export class BrowserController {
|
|
|
1104
1152
|
// not found — fall through to error
|
|
1105
1153
|
}
|
|
1106
1154
|
}
|
|
1107
|
-
throw new Error(`combobox ${triggerSelector}
|
|
1155
|
+
throw new Error(`combobox ${triggerSelector}` +
|
|
1156
|
+
(normalizedSelector !== triggerSelector ? ` (normalized to ${normalizedSelector})` : "") +
|
|
1157
|
+
`: no options found after click. ` +
|
|
1108
1158
|
`Tried: ${triedDescriptors.join(", ")}. ` +
|
|
1109
1159
|
`The trigger may not have opened a popover, or the popover uses ` +
|
|
1110
1160
|
`an option pattern this executor doesn't recognize.`);
|
|
1111
1161
|
}
|
|
1162
|
+
// 0.8.2-rc.11 — resolve a `<label for="X">` selector to `#X` so the
|
|
1163
|
+
// executor lands on the actual input rather than the label decoration.
|
|
1164
|
+
// The planner-emitted inventory line for Sentry's permission grid
|
|
1165
|
+
// sometimes targets the label (the visible text is "Project", which
|
|
1166
|
+
// lives on the <label>, not the <input>); a click on a label only
|
|
1167
|
+
// synthetically focuses its `for` target, which is insufficient to
|
|
1168
|
+
// open a react-select menu. Returns the original selector unchanged
|
|
1169
|
+
// when the resolution doesn't apply (target isn't a label, has no
|
|
1170
|
+
// `for`, or the `for`-id doesn't resolve to an input).
|
|
1171
|
+
async resolveLabelToInput(selector) {
|
|
1172
|
+
if (!this.page)
|
|
1173
|
+
throw new Error("Browser not started");
|
|
1174
|
+
try {
|
|
1175
|
+
const resolvedId = await this.page
|
|
1176
|
+
.locator(selector)
|
|
1177
|
+
.first()
|
|
1178
|
+
.evaluate((node) => {
|
|
1179
|
+
if (!(node instanceof HTMLLabelElement))
|
|
1180
|
+
return null;
|
|
1181
|
+
const forAttr = node.htmlFor;
|
|
1182
|
+
if (forAttr.length === 0)
|
|
1183
|
+
return null;
|
|
1184
|
+
const target = node.ownerDocument.getElementById(forAttr);
|
|
1185
|
+
if (target === null)
|
|
1186
|
+
return null;
|
|
1187
|
+
// Only redirect when the target is input/textarea/select. A
|
|
1188
|
+
// label pointing at a non-form element (rare; React Aria
|
|
1189
|
+
// does it for a labelled-by relationship) shouldn't trigger
|
|
1190
|
+
// the redirect.
|
|
1191
|
+
const tag = target.tagName.toLowerCase();
|
|
1192
|
+
if (tag !== "input" && tag !== "textarea" && tag !== "select") {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
return forAttr;
|
|
1196
|
+
});
|
|
1197
|
+
if (resolvedId === null)
|
|
1198
|
+
return selector;
|
|
1199
|
+
// CSS-escape the id so unusual characters (Sentry's `--` separator
|
|
1200
|
+
// is fine, but the helper is defensive against future ids that
|
|
1201
|
+
// include `.`, spaces, …) don't break the locator.
|
|
1202
|
+
const escaped = (typeof globalThis.CSS?.escape ===
|
|
1203
|
+
"function"
|
|
1204
|
+
? globalThis.CSS.escape(resolvedId)
|
|
1205
|
+
: resolvedId.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1"));
|
|
1206
|
+
return `#${escaped}`;
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
return selector;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
// 0.8.2-rc.11 — keyboard-driven react-select interaction. The
|
|
1213
|
+
// trigger is the inner <input>; opening the menu via mouse click
|
|
1214
|
+
// alone isn't reliable on every react-select instance (Sentry's
|
|
1215
|
+
// permission grid). Sequence:
|
|
1216
|
+
// 1. focus the trigger (the click already happened in
|
|
1217
|
+
// selectFromCombobox, but a defensive .focus() handles the
|
|
1218
|
+
// case where the click went to a sibling overlay).
|
|
1219
|
+
// 2. press Alt+ArrowDown — react-select binds this to open the
|
|
1220
|
+
// menu and select the first option.
|
|
1221
|
+
// 3. if a matcher was given, type its first 1-3 letters to filter
|
|
1222
|
+
// the menu down to the right option, then press Enter to
|
|
1223
|
+
// commit.
|
|
1224
|
+
// 4. if no matcher, ArrowDown was already issued — press Enter to
|
|
1225
|
+
// commit the first option.
|
|
1226
|
+
// Verify via the input's aria-activedescendant or value attribute
|
|
1227
|
+
// changing (react-select updates one or the other on selection).
|
|
1228
|
+
// Returns true on success, false when the page didn't react.
|
|
1229
|
+
async tryReactSelectKeyboardPick(triggerSelector, optionMatcher) {
|
|
1230
|
+
if (!this.page)
|
|
1231
|
+
throw new Error("Browser not started");
|
|
1232
|
+
const triggerLocator = this.page.locator(triggerSelector);
|
|
1233
|
+
try {
|
|
1234
|
+
const tagName = await triggerLocator
|
|
1235
|
+
.first()
|
|
1236
|
+
.evaluate((node) => node.tagName.toLowerCase());
|
|
1237
|
+
// Limit this path to input-typed triggers; native <select> and
|
|
1238
|
+
// <button role="combobox"> are handled by other tiers. The
|
|
1239
|
+
// selectFromCombobox caller has already returned for matching
|
|
1240
|
+
// [role="option"] tiers, so we only reach here on patterns where
|
|
1241
|
+
// the trigger is an input.
|
|
1242
|
+
if (tagName !== "input")
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
try {
|
|
1249
|
+
await triggerLocator.first().focus({ timeout: 1500 });
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
// Snapshot the input's relevant attributes BEFORE opening so we
|
|
1255
|
+
// can verify that the pick actually committed.
|
|
1256
|
+
const before = await triggerLocator
|
|
1257
|
+
.first()
|
|
1258
|
+
.evaluate((node) => ({
|
|
1259
|
+
activedescendant: node.getAttribute("aria-activedescendant") ?? "",
|
|
1260
|
+
value: node instanceof HTMLInputElement ? node.value : "",
|
|
1261
|
+
// react-select 5 mirrors the selected value into the closest
|
|
1262
|
+
// .css-{hash}-singleValue node; grab the trigger's surrounding
|
|
1263
|
+
// text so a successful pick produces an observable change.
|
|
1264
|
+
surroundingText: node.parentElement?.parentElement?.parentElement?.textContent ?? "",
|
|
1265
|
+
}))
|
|
1266
|
+
.catch(() => ({ activedescendant: "", value: "", surroundingText: "" }));
|
|
1267
|
+
// Press Alt+ArrowDown to open + highlight the first option, then
|
|
1268
|
+
// if a matcher exists, type to filter, then Enter.
|
|
1269
|
+
try {
|
|
1270
|
+
await this.page.keyboard.press("Alt+ArrowDown");
|
|
1271
|
+
}
|
|
1272
|
+
catch {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
// Wait briefly for the menu to render.
|
|
1276
|
+
await this.wait(0.4);
|
|
1277
|
+
if (optionMatcher !== undefined && optionMatcher.length > 0) {
|
|
1278
|
+
// Type a few characters to filter; react-select narrows on each
|
|
1279
|
+
// keystroke. Capping at 6 keeps the input from overshooting on
|
|
1280
|
+
// a long matcher when the first few characters already narrow
|
|
1281
|
+
// to a single option ("Admin" → typing "Adm" is enough).
|
|
1282
|
+
const typed = optionMatcher.slice(0, 6);
|
|
1283
|
+
try {
|
|
1284
|
+
await triggerLocator.first().pressSequentially(typed, { delay: 25 });
|
|
1285
|
+
}
|
|
1286
|
+
catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
await this.wait(0.35);
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
await this.page.keyboard.press("Enter");
|
|
1293
|
+
}
|
|
1294
|
+
catch {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
await this.wait(0.5);
|
|
1298
|
+
const after = await triggerLocator
|
|
1299
|
+
.first()
|
|
1300
|
+
.evaluate((node) => ({
|
|
1301
|
+
activedescendant: node.getAttribute("aria-activedescendant") ?? "",
|
|
1302
|
+
value: node instanceof HTMLInputElement ? node.value : "",
|
|
1303
|
+
surroundingText: node.parentElement?.parentElement?.parentElement?.textContent ?? "",
|
|
1304
|
+
}))
|
|
1305
|
+
.catch(() => ({ activedescendant: "", value: "", surroundingText: "" }));
|
|
1306
|
+
// A successful pick produces at least one observable change.
|
|
1307
|
+
// react-select clears the input's value once a selection commits
|
|
1308
|
+
// (the chosen label moves into a sibling singleValue node), so the
|
|
1309
|
+
// surrounding-text diff is the strongest signal.
|
|
1310
|
+
if (before.surroundingText !== after.surroundingText)
|
|
1311
|
+
return true;
|
|
1312
|
+
if (before.activedescendant !== after.activedescendant)
|
|
1313
|
+
return true;
|
|
1314
|
+
if (before.value !== after.value)
|
|
1315
|
+
return true;
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1112
1318
|
// F11: pick an option from a Playwright Locator already-narrowed to
|
|
1113
1319
|
// candidates. Matcher → filter by hasText (case-insensitive by
|
|
1114
1320
|
// default in Playwright). No matcher → first.
|
|
@@ -2010,8 +2216,37 @@ export class BrowserController {
|
|
|
2010
2216
|
if (trimmed.length === 0)
|
|
2011
2217
|
return;
|
|
2012
2218
|
const masked = isMaskedShape(trimmed);
|
|
2013
|
-
if (!masked && !isCredentialShape(trimmed))
|
|
2219
|
+
if (!masked && !isCredentialShape(trimmed)) {
|
|
2220
|
+
// 0.8.2-rc.17 — when the whole text-node string has
|
|
2221
|
+
// whitespace (Cloudinary's "Cloud name: dlq4xgrca" sits
|
|
2222
|
+
// in a SINGLE <div> with the label and value glued
|
|
2223
|
+
// together), isCredentialShape rejects the whole string.
|
|
2224
|
+
// Try to split on the canonical label-value separator
|
|
2225
|
+
// patterns ("Label: value", "Label = value", "Label\nvalue")
|
|
2226
|
+
// and re-evaluate each side. The token side gets the
|
|
2227
|
+
// candidate slot; the label side already lives on its own
|
|
2228
|
+
// (we don't need to push it). First-wins on duplicates.
|
|
2229
|
+
const split = /^([A-Za-z][A-Za-z _-]{1,40}?)\s*[:=]\s*([A-Za-z0-9._\-]{4,256})$/.exec(trimmed);
|
|
2230
|
+
if (split === null)
|
|
2231
|
+
return;
|
|
2232
|
+
const valueToken = split[2];
|
|
2233
|
+
if (valueToken === undefined)
|
|
2234
|
+
return;
|
|
2235
|
+
if (!isCredentialShape(valueToken))
|
|
2236
|
+
return;
|
|
2237
|
+
if (seen.has(valueToken))
|
|
2238
|
+
return;
|
|
2239
|
+
seen.add(valueToken);
|
|
2240
|
+
const c = centerOf(el);
|
|
2241
|
+
const label = findNearestLabel(c.x, c.y);
|
|
2242
|
+
out.push({
|
|
2243
|
+
value: valueToken,
|
|
2244
|
+
label,
|
|
2245
|
+
isMasked: false,
|
|
2246
|
+
hasRevealButton: false,
|
|
2247
|
+
});
|
|
2014
2248
|
return;
|
|
2249
|
+
}
|
|
2015
2250
|
if (seen.has(trimmed))
|
|
2016
2251
|
return;
|
|
2017
2252
|
seen.add(trimmed);
|