browser-pilot 0.0.12 → 0.0.14

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.
@@ -2,6 +2,230 @@ import {
2
2
  CDPError
3
3
  } from "./chunk-JXAUPHZM.mjs";
4
4
 
5
+ // src/actions/executor.ts
6
+ import * as fs from "fs";
7
+ import { join } from "path";
8
+
9
+ // src/recording/redaction.ts
10
+ var REDACTED_VALUE = "[REDACTED]";
11
+ var SENSITIVE_AUTOCOMPLETE_TOKENS = [
12
+ "current-password",
13
+ "new-password",
14
+ "one-time-code",
15
+ "cc-number",
16
+ "cc-csc",
17
+ "cc-exp",
18
+ "cc-exp-month",
19
+ "cc-exp-year"
20
+ ];
21
+ function autocompleteTokens(autocomplete) {
22
+ if (!autocomplete) return [];
23
+ return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
24
+ }
25
+ function isSensitiveFieldMetadata(metadata) {
26
+ if (!metadata) return false;
27
+ if (metadata.sensitiveValue) return true;
28
+ const inputType = metadata.inputType?.toLowerCase();
29
+ if (inputType === "password" || inputType === "hidden") {
30
+ return true;
31
+ }
32
+ const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
33
+ return autocompleteTokens(metadata.autocomplete).some(
34
+ (token) => sensitiveAutocompleteTokens.has(token)
35
+ );
36
+ }
37
+ function redactValueForRecording(value, metadata) {
38
+ if (value === void 0) return void 0;
39
+ return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
40
+ }
41
+
42
+ // src/browser/action-highlight.ts
43
+ var HIGHLIGHT_STYLES = {
44
+ click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
45
+ fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
46
+ type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
47
+ select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
48
+ hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
49
+ scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
50
+ navigate: { outline: "none", badge: "#4caf50" },
51
+ submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
52
+ "assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
53
+ "assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
54
+ evaluate: { outline: "none", badge: "#ffc107" },
55
+ focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
56
+ };
57
+ function buildHighlightScript(options) {
58
+ const style = HIGHLIGHT_STYLES[options.kind];
59
+ const label = options.label ? options.label.slice(0, 80) : void 0;
60
+ const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
61
+ return `(function() {
62
+ // Remove any existing highlight
63
+ var existing = document.getElementById('__bp-action-highlight');
64
+ if (existing) existing.remove();
65
+
66
+ var container = document.createElement('div');
67
+ container.id = '__bp-action-highlight';
68
+ container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
69
+
70
+ ${options.bbox ? `
71
+ // Element outline
72
+ var outline = document.createElement('div');
73
+ outline.style.cssText = 'position:fixed;' +
74
+ 'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
75
+ 'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
76
+ '${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
77
+ 'pointer-events:none;box-sizing:border-box;';
78
+ container.appendChild(outline);
79
+ ` : ""}
80
+
81
+ ${options.point && style.marker === "crosshair" ? `
82
+ // Crosshair at click point
83
+ var hLine = document.createElement('div');
84
+ hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
85
+ 'width:24px;height:2px;background:${style.badge};pointer-events:none;';
86
+ var vLine = document.createElement('div');
87
+ vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
88
+ 'width:2px;height:24px;background:${style.badge};pointer-events:none;';
89
+ // Dot at center
90
+ var dot = document.createElement('div');
91
+ dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
92
+ 'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
93
+ container.appendChild(hLine);
94
+ container.appendChild(vLine);
95
+ container.appendChild(dot);
96
+ ` : ""}
97
+
98
+ ${label ? `
99
+ // Badge with label
100
+ var badge = document.createElement('div');
101
+ badge.style.cssText = 'position:fixed;' +
102
+ ${options.bbox ? `'left:${options.bbox.x}px;top:${Math.max(0, options.bbox.y - 28)}px;'` : options.kind === "navigate" ? "'left:50%;top:8px;transform:translateX(-50%);'" : "'right:8px;top:8px;'"} +
103
+ 'background:${style.badge};color:white;padding:4px 8px;' +
104
+ 'font-family:monospace;font-size:12px;font-weight:bold;' +
105
+ 'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
106
+ 'pointer-events:none;';
107
+ badge.textContent = '${escapedLabel}';
108
+ container.appendChild(badge);
109
+ ` : ""}
110
+
111
+ ${style.marker === "check" && options.bbox ? `
112
+ // Checkmark
113
+ var check = document.createElement('div');
114
+ check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
115
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
116
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
117
+ check.textContent = '\\u2713';
118
+ container.appendChild(check);
119
+ ` : ""}
120
+
121
+ ${style.marker === "cross" && options.bbox ? `
122
+ // Cross mark
123
+ var cross = document.createElement('div');
124
+ cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
125
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
126
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
127
+ cross.textContent = '\\u2717';
128
+ container.appendChild(cross);
129
+ ` : ""}
130
+
131
+ document.body.appendChild(container);
132
+ window.__bpRemoveActionHighlight = function() {
133
+ var el = document.getElementById('__bp-action-highlight');
134
+ if (el) el.remove();
135
+ delete window.__bpRemoveActionHighlight;
136
+ };
137
+ })();`;
138
+ }
139
+ async function injectActionHighlight(page, options) {
140
+ try {
141
+ await page.evaluate(buildHighlightScript(options));
142
+ } catch {
143
+ }
144
+ }
145
+ async function removeActionHighlight(page) {
146
+ try {
147
+ await page.evaluate(`(function() {
148
+ if (window.__bpRemoveActionHighlight) {
149
+ window.__bpRemoveActionHighlight();
150
+ }
151
+ })()`);
152
+ } catch {
153
+ }
154
+ }
155
+ function stepToHighlightKind(step) {
156
+ switch (step.action) {
157
+ case "click":
158
+ return "click";
159
+ case "fill":
160
+ return "fill";
161
+ case "type":
162
+ return "type";
163
+ case "select":
164
+ return "select";
165
+ case "hover":
166
+ return "hover";
167
+ case "scroll":
168
+ return "scroll";
169
+ case "goto":
170
+ return "navigate";
171
+ case "submit":
172
+ return "submit";
173
+ case "focus":
174
+ return "focus";
175
+ case "evaluate":
176
+ case "press":
177
+ case "shortcut":
178
+ return "evaluate";
179
+ case "assertVisible":
180
+ case "assertExists":
181
+ case "assertText":
182
+ case "assertUrl":
183
+ case "assertValue":
184
+ return step.success ? "assert-pass" : "assert-fail";
185
+ // Observation-only actions — no highlight
186
+ case "wait":
187
+ case "snapshot":
188
+ case "forms":
189
+ case "text":
190
+ case "screenshot":
191
+ case "newTab":
192
+ case "closeTab":
193
+ case "switchFrame":
194
+ case "switchToMain":
195
+ return null;
196
+ default:
197
+ return null;
198
+ }
199
+ }
200
+ function getHighlightLabel(step, result, targetMetadata) {
201
+ switch (step.action) {
202
+ case "fill":
203
+ case "type":
204
+ return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
205
+ case "select":
206
+ return redactValueForRecording(
207
+ typeof step.value === "string" ? step.value : void 0,
208
+ targetMetadata
209
+ );
210
+ case "goto":
211
+ return step.url;
212
+ case "evaluate":
213
+ return "JS";
214
+ case "press":
215
+ return step.key;
216
+ case "shortcut":
217
+ return step.combo;
218
+ case "assertText":
219
+ case "assertUrl":
220
+ case "assertValue":
221
+ case "assertVisible":
222
+ case "assertExists":
223
+ return result.success ? "\u2713" : "\u2717";
224
+ default:
225
+ return void 0;
226
+ }
227
+ }
228
+
5
229
  // src/browser/actionability.ts
6
230
  var ActionabilityError = class extends Error {
7
231
  failureType;
@@ -616,6 +840,13 @@ var NavigationError = class extends Error {
616
840
 
617
841
  // src/actions/executor.ts
618
842
  var DEFAULT_TIMEOUT = 3e4;
843
+ var DEFAULT_RECORDING_SKIP_ACTIONS = [
844
+ "wait",
845
+ "snapshot",
846
+ "forms",
847
+ "text",
848
+ "screenshot"
849
+ ];
619
850
  function classifyFailure(error) {
620
851
  if (error instanceof ElementNotFoundError) {
621
852
  return { reason: "missing" };
@@ -695,6 +926,9 @@ var BatchExecutor = class {
695
926
  const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
696
927
  const results = [];
697
928
  const startTime = Date.now();
929
+ const recording = options.record ? this.createRecordingContext(options.record) : null;
930
+ const startUrl = recording ? await this.getPageUrlSafe() : "";
931
+ let stoppedAtIndex;
698
932
  for (let i = 0; i < steps.length; i++) {
699
933
  const step = steps[i];
700
934
  const stepStart = Date.now();
@@ -707,8 +941,9 @@ var BatchExecutor = class {
707
941
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
708
942
  }
709
943
  try {
944
+ this.page.resetLastActionPosition();
710
945
  const result = await this.executeStep(step, timeout);
711
- results.push({
946
+ const stepResult = {
712
947
  index: i,
713
948
  action: step.action,
714
949
  selector: step.selector,
@@ -716,8 +951,15 @@ var BatchExecutor = class {
716
951
  success: true,
717
952
  durationMs: Date.now() - stepStart,
718
953
  result: result.value,
719
- text: result.text
720
- });
954
+ text: result.text,
955
+ timestamp: Date.now(),
956
+ coordinates: this.page.getLastActionCoordinates() ?? void 0,
957
+ boundingBox: this.page.getLastActionBoundingBox() ?? void 0
958
+ };
959
+ if (recording && !recording.skipActions.has(step.action)) {
960
+ await this.captureRecordingFrame(step, stepResult, recording);
961
+ }
962
+ results.push(stepResult);
721
963
  succeeded = true;
722
964
  break;
723
965
  } catch (error) {
@@ -738,7 +980,7 @@ var BatchExecutor = class {
738
980
  } catch {
739
981
  }
740
982
  }
741
- results.push({
983
+ const failedResult = {
742
984
  index: i,
743
985
  action: step.action,
744
986
  selector: step.selector,
@@ -748,24 +990,176 @@ var BatchExecutor = class {
748
990
  hints,
749
991
  failureReason: reason,
750
992
  coveringElement,
751
- suggestion: getSuggestion(reason)
752
- });
993
+ suggestion: getSuggestion(reason),
994
+ timestamp: Date.now()
995
+ };
996
+ if (recording && !recording.skipActions.has(step.action)) {
997
+ await this.captureRecordingFrame(step, failedResult, recording);
998
+ }
999
+ results.push(failedResult);
753
1000
  if (onFail === "stop" && !step.optional) {
754
- return {
755
- success: false,
756
- stoppedAtIndex: i,
757
- steps: results,
758
- totalDurationMs: Date.now() - startTime
759
- };
1001
+ stoppedAtIndex = i;
1002
+ break;
760
1003
  }
761
1004
  }
762
1005
  }
763
- const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
1006
+ const totalDurationMs = Date.now() - startTime;
1007
+ const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
1008
+ let recordingManifest;
1009
+ if (recording) {
1010
+ recordingManifest = await this.writeRecordingManifest(
1011
+ recording,
1012
+ startTime,
1013
+ startUrl,
1014
+ allSuccess
1015
+ );
1016
+ }
764
1017
  return {
765
1018
  success: allSuccess,
1019
+ stoppedAtIndex,
766
1020
  steps: results,
767
- totalDurationMs: Date.now() - startTime
1021
+ totalDurationMs,
1022
+ recordingManifest
1023
+ };
1024
+ }
1025
+ createRecordingContext(record) {
1026
+ const baseDir = record.outputDir ?? join(process.cwd(), ".browser-pilot");
1027
+ const screenshotDir = join(baseDir, "screenshots");
1028
+ const manifestPath = join(baseDir, "recording.json");
1029
+ let existingFrames = [];
1030
+ try {
1031
+ const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1032
+ if (existing.frames && Array.isArray(existing.frames)) {
1033
+ existingFrames = existing.frames;
1034
+ }
1035
+ } catch {
1036
+ }
1037
+ fs.mkdirSync(screenshotDir, { recursive: true });
1038
+ return {
1039
+ baseDir,
1040
+ screenshotDir,
1041
+ sessionId: record.sessionId ?? this.page.targetId,
1042
+ frames: existingFrames,
1043
+ format: record.format ?? "webp",
1044
+ quality: Math.max(0, Math.min(100, record.quality ?? 40)),
1045
+ highlights: record.highlights !== false,
1046
+ skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
1047
+ };
1048
+ }
1049
+ async getPageUrlSafe() {
1050
+ try {
1051
+ return await this.page.url();
1052
+ } catch {
1053
+ return "";
1054
+ }
1055
+ }
1056
+ /**
1057
+ * Capture a recording screenshot frame with optional highlight overlay
1058
+ */
1059
+ async captureRecordingFrame(step, stepResult, recording) {
1060
+ const targetMetadata = this.page.getLastActionTargetMetadata();
1061
+ let highlightInjected = false;
1062
+ try {
1063
+ const ts = Date.now();
1064
+ const seq = String(recording.frames.length + 1).padStart(4, "0");
1065
+ const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
1066
+ const filepath = join(recording.screenshotDir, filename);
1067
+ if (recording.highlights) {
1068
+ const kind = stepToHighlightKind(stepResult);
1069
+ if (kind) {
1070
+ await injectActionHighlight(this.page, {
1071
+ kind,
1072
+ bbox: stepResult.boundingBox,
1073
+ point: stepResult.coordinates,
1074
+ label: getHighlightLabel(step, stepResult, targetMetadata)
1075
+ });
1076
+ highlightInjected = true;
1077
+ }
1078
+ }
1079
+ const base64 = await this.page.screenshot({
1080
+ format: recording.format,
1081
+ quality: recording.quality
1082
+ });
1083
+ const buffer = Buffer.from(base64, "base64");
1084
+ fs.writeFileSync(filepath, buffer);
1085
+ stepResult.screenshotPath = filepath;
1086
+ let pageUrl;
1087
+ let pageTitle;
1088
+ try {
1089
+ pageUrl = await this.page.url();
1090
+ pageTitle = await this.page.title();
1091
+ } catch {
1092
+ }
1093
+ recording.frames.push({
1094
+ seq: recording.frames.length + 1,
1095
+ timestamp: ts,
1096
+ action: stepResult.action,
1097
+ selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
1098
+ value: redactValueForRecording(
1099
+ typeof step.value === "string" ? step.value : void 0,
1100
+ targetMetadata
1101
+ ),
1102
+ url: step.url,
1103
+ coordinates: stepResult.coordinates,
1104
+ boundingBox: stepResult.boundingBox,
1105
+ success: stepResult.success,
1106
+ durationMs: stepResult.durationMs,
1107
+ error: stepResult.error,
1108
+ screenshot: filename,
1109
+ pageUrl,
1110
+ pageTitle
1111
+ });
1112
+ } catch {
1113
+ } finally {
1114
+ if (recording.highlights || highlightInjected) {
1115
+ await removeActionHighlight(this.page);
1116
+ }
1117
+ }
1118
+ }
1119
+ /**
1120
+ * Write recording manifest to disk
1121
+ */
1122
+ async writeRecordingManifest(recording, startTime, startUrl, success) {
1123
+ let endUrl = startUrl;
1124
+ let viewport = { width: 1280, height: 720 };
1125
+ try {
1126
+ endUrl = await this.page.url();
1127
+ } catch {
1128
+ }
1129
+ try {
1130
+ const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
1131
+ viewport = {
1132
+ width: metrics.cssVisualViewport.clientWidth,
1133
+ height: metrics.cssVisualViewport.clientHeight
1134
+ };
1135
+ } catch {
1136
+ }
1137
+ const manifestPath = join(recording.baseDir, "recording.json");
1138
+ let recordedAt = new Date(startTime).toISOString();
1139
+ let originalStartUrl = startUrl;
1140
+ try {
1141
+ const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1142
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
1143
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
1144
+ } catch {
1145
+ }
1146
+ const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
1147
+ const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
1148
+ const manifest = {
1149
+ version: 1,
1150
+ recordedAt,
1151
+ sessionId: recording.sessionId,
1152
+ startUrl: originalStartUrl,
1153
+ endUrl,
1154
+ viewport,
1155
+ format: recording.format,
1156
+ quality: recording.quality,
1157
+ totalDurationMs,
1158
+ success,
1159
+ frames: recording.frames
768
1160
  };
1161
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1162
+ return manifestPath;
769
1163
  }
770
1164
  /**
771
1165
  * Execute a single step
@@ -926,6 +1320,9 @@ var BatchExecutor = class {
926
1320
  const snapshot = await this.page.snapshot();
927
1321
  return { value: snapshot };
928
1322
  }
1323
+ case "forms": {
1324
+ return { value: await this.page.forms() };
1325
+ }
929
1326
  case "screenshot": {
930
1327
  const data = await this.page.screenshot({
931
1328
  format: step.format,
@@ -945,6 +1342,21 @@ var BatchExecutor = class {
945
1342
  const text = await this.page.text(selector);
946
1343
  return { text, selectorUsed: selector };
947
1344
  }
1345
+ case "newTab": {
1346
+ const { targetId } = await this.page.cdpClient.send(
1347
+ "Target.createTarget",
1348
+ {
1349
+ url: step.url ?? "about:blank"
1350
+ },
1351
+ null
1352
+ );
1353
+ return { value: { targetId } };
1354
+ }
1355
+ case "closeTab": {
1356
+ const targetId = step.targetId ?? this.page.targetId;
1357
+ await this.page.cdpClient.send("Target.closeTarget", { targetId }, null);
1358
+ return { value: { targetId, closedCurrent: targetId === this.page.targetId } };
1359
+ }
948
1360
  case "switchFrame": {
949
1361
  if (!step.selector) throw new Error("switchFrame requires selector");
950
1362
  await this.page.switchToFrame(step.selector, { timeout, optional });
@@ -1059,10 +1471,15 @@ var BatchExecutor = class {
1059
1471
  snap: "snapshot",
1060
1472
  accessibility: "snapshot",
1061
1473
  a11y: "snapshot",
1474
+ formslist: "forms",
1062
1475
  image: "screenshot",
1063
1476
  pic: "screenshot",
1064
1477
  frame: "switchFrame",
1065
1478
  iframe: "switchFrame",
1479
+ newtab: "newTab",
1480
+ opentab: "newTab",
1481
+ createtab: "newTab",
1482
+ closetab: "closeTab",
1066
1483
  assert_visible: "assertVisible",
1067
1484
  assert_exists: "assertExists",
1068
1485
  assert_text: "assertText",
@@ -1076,7 +1493,7 @@ var BatchExecutor = class {
1076
1493
  };
1077
1494
  const suggestion = aliases[action.toLowerCase()];
1078
1495
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
1079
- const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
1496
+ const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
1080
1497
  throw new Error(`Unknown action "${action}".${hint}
1081
1498
 
1082
1499
  Valid actions: ${valid}`);
@@ -1147,6 +1564,11 @@ var ACTION_ALIASES = {
1147
1564
  pic: "screenshot",
1148
1565
  frame: "switchFrame",
1149
1566
  iframe: "switchFrame",
1567
+ formslist: "forms",
1568
+ newtab: "newTab",
1569
+ opentab: "newTab",
1570
+ createtab: "newTab",
1571
+ closetab: "closeTab",
1150
1572
  assert_visible: "assertVisible",
1151
1573
  assert_exists: "assertExists",
1152
1574
  assert_text: "assertText",
@@ -1183,7 +1605,8 @@ var PROPERTY_ALIASES = {
1183
1605
  button: "key",
1184
1606
  address: "url",
1185
1607
  page: "url",
1186
- path: "url"
1608
+ path: "url",
1609
+ tabId: "targetId"
1187
1610
  };
1188
1611
  var ACTION_RULES = {
1189
1612
  goto: {
@@ -1284,6 +1707,10 @@ var ACTION_RULES = {
1284
1707
  fullPage: { type: "boolean" }
1285
1708
  }
1286
1709
  },
1710
+ forms: {
1711
+ required: {},
1712
+ optional: {}
1713
+ },
1287
1714
  evaluate: {
1288
1715
  required: { value: { type: "string" } },
1289
1716
  optional: {}
@@ -1298,6 +1725,18 @@ var ACTION_RULES = {
1298
1725
  required: { selector: { type: "string|string[]" } },
1299
1726
  optional: {}
1300
1727
  },
1728
+ newTab: {
1729
+ required: {},
1730
+ optional: {
1731
+ url: { type: "string" }
1732
+ }
1733
+ },
1734
+ closeTab: {
1735
+ required: {},
1736
+ optional: {
1737
+ targetId: { type: "string" }
1738
+ }
1739
+ },
1301
1740
  switchToMain: {
1302
1741
  required: {},
1303
1742
  optional: {}
@@ -1340,6 +1779,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
1340
1779
  "selector",
1341
1780
  "url",
1342
1781
  "value",
1782
+ "targetId",
1343
1783
  "key",
1344
1784
  "combo",
1345
1785
  "modifiers",
@@ -1494,15 +1934,22 @@ function validateSteps(steps) {
1494
1934
  const rule = ACTION_RULES[action];
1495
1935
  for (const key of Object.keys(obj)) {
1496
1936
  if (key === "action") continue;
1497
- if (!KNOWN_STEP_FIELDS.has(key)) {
1498
- const suggestion = suggestProperty(key);
1499
- errors.push({
1500
- stepIndex: i,
1501
- field: key,
1502
- message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
1503
- suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
1504
- });
1937
+ if (KNOWN_STEP_FIELDS.has(key)) continue;
1938
+ const canonical = PROPERTY_ALIASES[key];
1939
+ if (canonical) {
1940
+ if (!(canonical in obj)) {
1941
+ obj[canonical] = obj[key];
1942
+ }
1943
+ delete obj[key];
1944
+ continue;
1505
1945
  }
1946
+ const suggestion = suggestProperty(key);
1947
+ errors.push({
1948
+ stepIndex: i,
1949
+ field: key,
1950
+ message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
1951
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
1952
+ });
1506
1953
  }
1507
1954
  for (const [field, fieldRule] of Object.entries(rule.required)) {
1508
1955
  if (!(field in obj) || obj[field] === void 0) {