pi-agent-browser-native 0.3.0 → 0.6.5

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 (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,4 +1,4 @@
1
- import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, optionalGlobalValueFlagConsumesNext } from "./argv-grammar.js";
1
+ import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, stripUpstreamGlobalFlags } from "./argv-grammar.js";
2
2
  import { isOpenNavigationCommand } from "./command-taxonomy.js";
3
3
  function isBooleanLiteral(token) {
4
4
  const normalized = token?.trim().toLowerCase();
@@ -33,6 +33,33 @@ export function extractCommandTokens(args) {
33
33
  const commandStartIndex = findCommandStartIndex(args);
34
34
  return commandStartIndex === undefined ? [] : args.slice(commandStartIndex);
35
35
  }
36
+ export function extractUpstreamCommandTokens(args) {
37
+ return stripUpstreamGlobalFlags(extractCommandTokens(args));
38
+ }
39
+ export function parseWaitCommandTokens(commandTokens) {
40
+ if (commandTokens[0] !== "wait")
41
+ return {};
42
+ const considered = commandTokens.slice(1).map((token, offset) => ({ index: offset + 1, token }));
43
+ const timeoutIndex = considered.findIndex((entry) => entry.token === "--timeout");
44
+ if (timeoutIndex >= 0)
45
+ considered.splice(timeoutIndex, Math.min(2, considered.length - timeoutIndex));
46
+ for (const flags of [["--url", "-u"], ["--load", "-l"], ["--fn", "-f"], ["--text", "-t"]]) {
47
+ const match = considered.find((entry) => flags.includes(entry.token));
48
+ if (match)
49
+ return { subcommand: match.token };
50
+ }
51
+ const download = considered.find((entry) => entry.token === "--download" || entry.token === "-d");
52
+ if (download) {
53
+ const downloadPathIndex = download.index + 1;
54
+ const candidate = commandTokens[downloadPathIndex];
55
+ return {
56
+ downloadPath: candidate && !candidate.startsWith("--") ? candidate : undefined,
57
+ downloadPathIndex: candidate && !candidate.startsWith("--") ? downloadPathIndex : undefined,
58
+ subcommand: download.token,
59
+ };
60
+ }
61
+ return { subcommand: considered[0]?.token };
62
+ }
36
63
  function getOpenCommandTarget(commandTokens) {
37
64
  for (let index = 1; index < commandTokens.length; index += 1) {
38
65
  const token = commandTokens[index];
@@ -51,10 +78,13 @@ function getOpenCommandTarget(commandTokens) {
51
78
  return undefined;
52
79
  }
53
80
  export function parseCommandInfoFromTokens(commandTokens) {
54
- const command = commandTokens[0];
81
+ const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
82
+ const command = upstreamCommandTokens[0];
55
83
  return {
56
84
  command,
57
- subcommand: isOpenNavigationCommand(command) ? getOpenCommandTarget(commandTokens) : commandTokens[1],
85
+ subcommand: isOpenNavigationCommand(command)
86
+ ? getOpenCommandTarget(upstreamCommandTokens)
87
+ : command === "wait" ? parseWaitCommandTokens(upstreamCommandTokens).subcommand : upstreamCommandTokens[1],
58
88
  };
59
89
  }
60
90
  export function parseCommandInfo(args) {
@@ -62,8 +92,10 @@ export function parseCommandInfo(args) {
62
92
  }
63
93
  export function parseArgvDescriptor(args) {
64
94
  const commandTokens = extractCommandTokens(args);
95
+ const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
65
96
  return {
66
97
  commandInfo: parseCommandInfoFromTokens(commandTokens),
67
98
  commandTokens,
99
+ upstreamCommandTokens,
68
100
  };
69
101
  }
@@ -12,6 +12,7 @@ export const GLOBAL_VALUE_FLAGS = [
12
12
  "--restore-check-fn",
13
13
  "--proxy",
14
14
  "--proxy-bypass",
15
+ "--ca-cert",
15
16
  "--headers",
16
17
  "--executable-path",
17
18
  "--extension",
@@ -37,6 +38,7 @@ export const GLOBAL_VALUE_FLAGS = [
37
38
  "--idle-timeout",
38
39
  ];
39
40
  export const COMMAND_VALUE_FLAGS = [
41
+ "--allowed-origins",
40
42
  "--baseline",
41
43
  "--body",
42
44
  "--categories",
@@ -47,6 +49,7 @@ export const COMMAND_VALUE_FLAGS = [
47
49
  "--domain",
48
50
  "--expires",
49
51
  "--filter",
52
+ "--frame",
50
53
  "--fn",
51
54
  "--label",
52
55
  "--load",
@@ -57,6 +60,7 @@ export const COMMAND_VALUE_FLAGS = [
57
60
  "--prefix",
58
61
  "--path",
59
62
  "--port",
63
+ "--params",
60
64
  "--resource-type",
61
65
  "--resource-types",
62
66
  "--sameSite",
@@ -90,6 +94,13 @@ export const GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES = new Set([
90
94
  "--ignore-https-errors",
91
95
  "--json",
92
96
  "--no-auto-dialog",
97
+ "--no-ca-cert",
98
+ "--no-pin-tab",
99
+ "--no-webmcp",
100
+ "--offline",
101
+ "--pin-tab",
102
+ "--quick",
103
+ "--fix",
93
104
  "--quiet",
94
105
  "-q",
95
106
  "--verbose",
@@ -148,7 +159,7 @@ export function canonicalizeAgentBrowserNamespace(value) {
148
159
  }
149
160
  return normalized.replace(/[-_]+$/u, "") || undefined;
150
161
  }
151
- function foldAgentBrowserFilesystemIdentity(value, platform) {
162
+ export function foldAgentBrowserFilesystemIdentity(value, platform) {
152
163
  if (platform !== "darwin" && platform !== "win32")
153
164
  return value;
154
165
  // APFS aliases include full Unicode folds such as ß/SS and ς/Σ, not just ASCII case.
@@ -160,7 +171,11 @@ export function getAgentBrowserSessionIdentityKey(sessionName, namespace, platfo
160
171
  const canonicalSessionName = foldAgentBrowserFilesystemIdentity(sessionName, platform);
161
172
  return identityNamespace ? `${identityNamespace}\0${canonicalSessionName}` : canonicalSessionName;
162
173
  }
163
- /** Mirror upstream 0.33.2 global parsing: full argv, no `--` sentinel, and only global value payloads are skipped. */
174
+ export function isAgentBrowserSessionIdentityKeyInNamespace(identityKey, namespace) {
175
+ const prefix = getAgentBrowserSessionIdentityKey("", namespace);
176
+ return prefix ? identityKey.startsWith(prefix) : !identityKey.includes("\0");
177
+ }
178
+ /** Mirror upstream global parsing: full argv, no `--` sentinel, and only global value payloads are skipped. */
164
179
  export function scanUpstreamGlobalFlagOccurrences(args, targetFlag) {
165
180
  const occurrences = [];
166
181
  for (let index = 0; index < args.length; index += 1) {
@@ -257,6 +272,39 @@ export function optionalGlobalValueFlagConsumesNext(flag, nextToken) {
257
272
  return false;
258
273
  return !isKnownCommandToken(nextToken);
259
274
  }
275
+ export function projectUpstreamGlobalFlags(args) {
276
+ const indices = [];
277
+ const tokens = [];
278
+ let seenCommand = false;
279
+ for (let index = 0; index < args.length; index += 1) {
280
+ const token = args[index];
281
+ if (token.startsWith("--restore="))
282
+ continue;
283
+ if (token === "--restore") {
284
+ if (!seenCommand && optionalGlobalValueFlagConsumesNext(token, args[index + 1]))
285
+ index += 1;
286
+ continue;
287
+ }
288
+ if (PREVALIDATED_VALUE_FLAGS.has(token)) {
289
+ index += 1;
290
+ continue;
291
+ }
292
+ if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(token)) {
293
+ if (["true", "false"].includes(args[index + 1] ?? ""))
294
+ index += 1;
295
+ continue;
296
+ }
297
+ tokens.push(token);
298
+ indices.push(index);
299
+ if (isKnownCommandToken(token))
300
+ seenCommand = true;
301
+ }
302
+ return { indices, tokens };
303
+ }
304
+ /** Mirror upstream clean_args: remove global flags wherever they appear before command parsing. */
305
+ export function stripUpstreamGlobalFlags(args) {
306
+ return projectUpstreamGlobalFlags(args).tokens;
307
+ }
260
308
  export function stripSessionlessShapeGlobalFlags(commandTokens) {
261
309
  const stripped = [];
262
310
  for (let index = 0; index < commandTokens.length; index += 1) {
@@ -0,0 +1,71 @@
1
+ import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
+ import { isCloseAllCommand, isCloseCommand } from "./command-taxonomy.js";
3
+ import { isRecord } from "./parsing.js";
4
+ function getRowBrowserLaunched(row) {
5
+ const result = isRecord(row.result) ? row.result : isRecord(row.data) ? row.data : undefined;
6
+ const lifecycle = isRecord(row.lifecycle) ? row.lifecycle : isRecord(result?.lifecycle) ? result.lifecycle : undefined;
7
+ const effectiveLaunch = isRecord(lifecycle?.effectiveLaunch) ? lifecycle.effectiveLaunch : undefined;
8
+ return typeof effectiveLaunch?.browserLaunched === "boolean" ? effectiveLaunch.browserLaunched : undefined;
9
+ }
10
+ export function batchHasSuccessfulCloseAll(data, fallbackCommands = []) {
11
+ if (!Array.isArray(data))
12
+ return false;
13
+ return data.some((row, index) => {
14
+ if (!isRecord(row) || row.success !== true)
15
+ return false;
16
+ const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
17
+ ? row.command
18
+ : fallbackCommands[index];
19
+ return rowCommand ? isCloseAllCommand(extractUpstreamCommandTokens(rowCommand)) : false;
20
+ });
21
+ }
22
+ export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
23
+ if (!Array.isArray(rows))
24
+ return undefined;
25
+ let sawClose = false;
26
+ let endsClosed = false;
27
+ let browserActiveAfterClose = false;
28
+ let recordingClosedAfterBatch = false;
29
+ let statePath;
30
+ for (const [index, row] of rows.entries()) {
31
+ if (!isRecord(row))
32
+ continue;
33
+ const stepSucceeded = row.success === true;
34
+ const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
35
+ ? row.command
36
+ : fallbackCommands[index];
37
+ const browserLaunched = getRowBrowserLaunched(row);
38
+ if (!rowCommand) {
39
+ if (sawClose && browserLaunched !== false) {
40
+ endsClosed = false;
41
+ browserActiveAfterClose = true;
42
+ recordingClosedAfterBatch = false;
43
+ }
44
+ continue;
45
+ }
46
+ const [command, subcommand] = extractUpstreamCommandTokens(rowCommand);
47
+ if (stepSucceeded && isCloseCommand(command)) {
48
+ sawClose = true;
49
+ endsClosed = true;
50
+ browserActiveAfterClose = false;
51
+ recordingClosedAfterBatch = true;
52
+ const result = isRecord(row.result) ? row.result : isRecord(row.data) ? row.data : undefined;
53
+ statePath = typeof result?.statePath === "string" ? result.statePath : undefined;
54
+ }
55
+ else if (sawClose && command === "record") {
56
+ if (browserLaunched !== false) {
57
+ endsClosed = false;
58
+ browserActiveAfterClose = true;
59
+ }
60
+ if (stepSucceeded && subcommand === "stop")
61
+ recordingClosedAfterBatch = true;
62
+ else if (stepSucceeded && browserActiveAfterClose && (subcommand === "start" || subcommand === "restart"))
63
+ recordingClosedAfterBatch = false;
64
+ }
65
+ else if (sawClose && browserLaunched !== false) {
66
+ endsClosed = false;
67
+ browserActiveAfterClose = true;
68
+ }
69
+ }
70
+ return sawClose ? { endsClosed, recordingClosedAfterBatch, statePath } : undefined;
71
+ }
@@ -5,8 +5,7 @@ const EMPTY_BOOLEAN_FLAGS = new Set();
5
5
  const JSON_BOOLEAN_FLAGS = new Set(["--json"]);
6
6
  const AUTH_SAVE_BOOLEAN_FLAGS = new Set(["--json", "--password-stdin"]);
7
7
  const AUTH_SAVE_VALUE_FLAGS = new Set(["--password", "--password-selector", "--submit-selector", "--url", "--username", "--username-selector"]);
8
- const DASHBOARD_SUBCOMMANDS = new Set(["start", "stop"]);
9
- const DASHBOARD_START_VALUE_FLAGS = new Set(["--port"]);
8
+ const DASHBOARD_VALUE_FLAGS = new Set(["--allowed-origins", "--port"]);
10
9
  const DOCTOR_BOOLEAN_FLAGS = new Set(["--fix", "--headed", "--json", "--offline", "--quick", "--webgpu"]);
11
10
  const INSTALL_BOOLEAN_FLAGS = new Set(["--with-deps", "-d"]);
12
11
  const STATE_SESSIONLESS_SUBCOMMANDS = new Set(["list", "show", "clear", "clean", "rename"]);
@@ -26,11 +25,9 @@ function isSessionlessAuthCommand(commandTokens) {
26
25
  }
27
26
  function isSessionlessDashboardCommand(commandTokens) {
28
27
  const [, subcommand, ...rest] = commandTokens;
29
- if (subcommand === undefined)
30
- return true;
31
- if (!DASHBOARD_SUBCOMMANDS.has(subcommand))
32
- return false;
33
- return subcommand === "start" ? hasOnlyOptionFlags(rest, JSON_BOOLEAN_FLAGS, DASHBOARD_START_VALUE_FLAGS) : rest.length === 0;
28
+ if (subcommand === "stop")
29
+ return rest.length === 0;
30
+ return hasOnlyOptionFlags(subcommand === "start" ? rest : commandTokens.slice(1), JSON_BOOLEAN_FLAGS, DASHBOARD_VALUE_FLAGS);
34
31
  }
35
32
  function isSessionlessStateCommand(commandTokens) {
36
33
  const [, subcommand, firstArg, secondArg, ...rest] = commandTokens;
@@ -96,5 +93,5 @@ function isSessionlessCommand(commandTokens) {
96
93
  return false;
97
94
  }
98
95
  export function needsManagedSession(descriptor) {
99
- return !isSessionlessCommand(descriptor.commandTokens);
96
+ return !isSessionlessCommand(descriptor.upstreamCommandTokens);
100
97
  }
@@ -1,6 +1,7 @@
1
1
  const ADDITIONAL_COMMAND_TOKENS = [
2
- "a11y", "auth", "chat", "clipboard", "confirm", "connect", "dashboard", "deny", "device", "dialog", "diff", "doctor", "errors", "eval", "find", "frame", "get", "highlight", "inspect", "install", "is", "mcp", "plugin", "plugins", "profiles", "profiler", "react", "record", "removeinitscript", "session", "set", "skills", "snapshot", "state", "stream", "trace", "upgrade", "vitals", "wait", "web-vitals", "window",
2
+ "a11y", "auth", "chat", "clipboard", "confirm", "connect", "dashboard", "deny", "device", "dialog", "diff", "doctor", "errors", "eval", "find", "frame", "get", "highlight", "inspect", "install", "is", "mcp", "plugin", "plugins", "profiles", "profiler", "react", "record", "removeinitscript", "session", "set", "skills", "snapshot", "state", "stream", "trace", "upgrade", "vitals", "wait", "web-vitals", "webmcp", "window",
3
3
  ];
4
+ const WEBMCP_PAGE_MUTATION_SUBCOMMANDS = new Set(["invoke", "result", "cancel"]);
4
5
  const COMMAND_CAPABILITIES = [
5
6
  {
6
7
  command: "back",
@@ -61,6 +62,10 @@ const COMMAND_CAPABILITIES = [
61
62
  invalidatesBatchRefs: true,
62
63
  triggersPostMutationSnapshot: true,
63
64
  },
65
+ {
66
+ command: "diff",
67
+ guardsPageRefs: true,
68
+ },
64
69
  {
65
70
  command: "download",
66
71
  eligibleForPageChangeSummary: true,
@@ -92,6 +97,10 @@ const COMMAND_CAPABILITIES = [
92
97
  command: "find",
93
98
  eligibleForElectronHealthProbe: true,
94
99
  },
100
+ {
101
+ command: "frame",
102
+ guardsPageRefs: true,
103
+ },
95
104
  {
96
105
  command: "focus",
97
106
  guardsPageRefs: true,
@@ -104,6 +113,14 @@ const COMMAND_CAPABILITIES = [
104
113
  navigationObservable: true,
105
114
  triggersPostMutationSnapshot: true,
106
115
  },
116
+ {
117
+ command: "get",
118
+ guardsPageRefs: true,
119
+ },
120
+ {
121
+ command: "highlight",
122
+ guardsPageRefs: true,
123
+ },
107
124
  {
108
125
  command: "hover",
109
126
  eligibleForPageChangeSummary: true,
@@ -111,6 +128,10 @@ const COMMAND_CAPABILITIES = [
111
128
  invalidatesBatchRefs: true,
112
129
  triggersPostMutationSnapshot: true,
113
130
  },
131
+ {
132
+ command: "is",
133
+ guardsPageRefs: true,
134
+ },
114
135
  {
115
136
  command: "keydown",
116
137
  eligibleForElectronHealthProbe: true,
@@ -185,10 +206,12 @@ const COMMAND_CAPABILITIES = [
185
206
  {
186
207
  command: "screenshot",
187
208
  eligibleForPageChangeSummary: true,
209
+ guardsPageRefs: true,
188
210
  },
189
211
  {
190
212
  command: "scroll",
191
213
  eligibleForPageChangeSummary: true,
214
+ guardsPageRefs: true,
192
215
  invalidatesBatchRefs: true,
193
216
  triggersPostMutationSnapshot: true,
194
217
  },
@@ -281,11 +304,14 @@ export function normalizeCommandName(command) {
281
304
  export function isCloseCommand(command) {
282
305
  return hasCommandCapability(command, "closesSession");
283
306
  }
307
+ export function isCloseAllCommand(commandTokens) {
308
+ return isCloseCommand(commandTokens[0]) && commandTokens.slice(1).includes("--all");
309
+ }
284
310
  export function isOpenNavigationCommand(command) {
285
311
  return hasCommandCapability(command, "openNavigation");
286
312
  }
287
- export function isReadOnlyDiagnosticSessionTargetCommand(command, _subcommand) {
288
- return hasCommandCapability(command, "readOnlyDiagnosticSessionTarget");
313
+ export function isReadOnlyDiagnosticSessionTargetCommand(command, subcommand) {
314
+ return hasCommandCapability(command, "readOnlyDiagnosticSessionTarget") || (command === "webmcp" && subcommand === "list");
289
315
  }
290
316
  export function isSessionTabPinningExcludedCommand(command) {
291
317
  return hasCommandCapability(command, "excludedFromPinning");
@@ -293,8 +319,19 @@ export function isSessionTabPinningExcludedCommand(command) {
293
319
  export function isSessionTabPostCommandCorrectionExcludedCommand(command) {
294
320
  return hasCommandCapability(command, "excludedFromPostCommandCorrection");
295
321
  }
296
- export function isRefInvalidatingBatchCommand(command) {
297
- return hasCommandCapability(command, "invalidatesBatchRefs");
322
+ /** Upstream 0.33.2 record start swaps to a fresh active page before its already-active check, so even a failed start can replace the page; record restart navigates the current page only when a URL operand (any fourth token, mirroring upstream's positional slot) is present. */
323
+ export function isRecordPageTransitionCommand(tokens) {
324
+ if (tokens[0] !== "record")
325
+ return false;
326
+ if (tokens[1] === "start")
327
+ return true;
328
+ return tokens[1] === "restart" && tokens.length >= 4;
329
+ }
330
+ export function isWebMcpPageMutationCommand(tokens) {
331
+ return isWebMcpPageMutation(tokens[0], tokens[1]);
332
+ }
333
+ export function isRefInvalidatingBatchCommand(step) {
334
+ return hasCommandCapability(step[0], "invalidatesBatchRefs") || isRecordPageTransitionCommand(step) || isWebMcpPageMutationCommand(step);
298
335
  }
299
336
  export function isRefGuardedCommand(command) {
300
337
  return hasCommandCapability(command, "guardsPageRefs");
@@ -302,17 +339,21 @@ export function isRefGuardedCommand(command) {
302
339
  export function isElectronPostCommandHealthCommand(command) {
303
340
  return hasCommandCapability(command, "eligibleForElectronHealthProbe");
304
341
  }
305
- export function isNavigationObservableCommandName(command) {
306
- return hasCommandCapability(command, "navigationObservable");
342
+ function isWebMcpPageMutation(command, subcommand) {
343
+ return command === "webmcp" && WEBMCP_PAGE_MUTATION_SUBCOMMANDS.has(subcommand ?? "");
344
+ }
345
+ export function isNavigationObservableCommandName(command, subcommand) {
346
+ return hasCommandCapability(command, "navigationObservable") || isWebMcpPageMutation(command, subcommand);
307
347
  }
308
348
  export function isUnverifiedPageTransitionCommand(command, subcommand) {
309
349
  return ["back", "connect", "eval", "forward", "reload"].includes(command ?? "")
310
350
  || (command === "state" && subcommand === "load")
311
- || (command === "tab" && subcommand !== undefined && !["list", "new"].includes(subcommand));
351
+ || (command === "tab" && subcommand !== undefined && !["list", "new"].includes(subcommand))
352
+ || isWebMcpPageMutation(command, subcommand);
312
353
  }
313
- export function isPageMutationCommand(command) {
314
- return hasCommandCapability(command, "triggersPostMutationSnapshot");
354
+ export function isPageMutationCommand(command, subcommand) {
355
+ return hasCommandCapability(command, "triggersPostMutationSnapshot") || isWebMcpPageMutation(command, subcommand);
315
356
  }
316
- export function isPageChangeSummaryCommand(command) {
317
- return hasCommandCapability(command, "eligibleForPageChangeSummary");
357
+ export function isPageChangeSummaryCommand(command, subcommand) {
358
+ return hasCommandCapability(command, "eligibleForPageChangeSummary") || isWebMcpPageMutation(command, subcommand);
318
359
  }
@@ -7,10 +7,11 @@ import { join, resolve } from "node:path";
7
7
  /** @typedef {"global" | "project" | "override"} ConfigLayerScope */
8
8
  /** @typedef {"literal" | "env" | "command"} CredentialSourceKind */
9
9
  /** @typedef {"exa" | "brave"} WebSearchProvider */
10
+ /** @typedef {"auto" | "fast" | "instant" | "deep-lite" | "deep" | "deep-reasoning"} ExaSearchType */
10
11
  /** @typedef {"exaApiKey" | "braveApiKey"} WebSearchProviderConfigKey */
11
12
  /** @typedef {{ provider: WebSearchProvider; apiKeyEnv: string; configKey: WebSearchProviderConfigKey; label: string }} WebSearchProviderDescriptor */
12
13
  /** @typedef {{ name: string; policy?: BrowserDefaultProfilePolicy }} BrowserDefaultProfileConfig */
13
- /** @typedef {{ enabled?: boolean; preferredProvider?: WebSearchProvider; braveApiKey?: string; exaApiKey?: string }} WebSearchConfig */
14
+ /** @typedef {{ enabled?: boolean; preferredProvider?: WebSearchProvider; defaultSearchType?: ExaSearchType; braveApiKey?: string; exaApiKey?: string }} WebSearchConfig */
14
15
  /** @typedef {{ defaultProfile?: BrowserDefaultProfileConfig; executablePath?: string }} BrowserConfig */
15
16
  /** @typedef {{ version?: 1; webSearch?: WebSearchConfig; browser?: BrowserConfig }} AgentBrowserConfig */
16
17
  /** @typedef {{ config: AgentBrowserConfig; path: string; scope: ConfigLayerScope }} ConfigLayer */
@@ -45,6 +46,8 @@ export const WEB_SEARCH_PROVIDER_DESCRIPTORS = Object.freeze({
45
46
  export const WEB_SEARCH_PROVIDERS = Object.freeze(["exa", "brave"]);
46
47
  /** @type {WebSearchProvider} */
47
48
  export const DEFAULT_WEB_SEARCH_PROVIDER = "exa";
49
+ /** @type {readonly ExaSearchType[]} */
50
+ export const EXA_SEARCH_TYPES = Object.freeze(["auto", "fast", "instant", "deep-lite", "deep", "deep-reasoning"]);
48
51
  /** @type {Readonly<Record<WebSearchProvider, WebSearchProviderConfigKey>>} */
49
52
  export const WEB_SEARCH_PROVIDER_CONFIG_KEYS = Object.freeze({
50
53
  exa: WEB_SEARCH_PROVIDER_DESCRIPTORS.exa.configKey,
@@ -182,6 +185,24 @@ export function validateWebSearchProvider(value, path, errors) {
182
185
  }
183
186
  return provider;
184
187
  }
188
+ /**
189
+ * @param {unknown} value
190
+ * @param {string} path
191
+ * @param {string[]} errors
192
+ * @returns {ExaSearchType | undefined}
193
+ */
194
+ function validateExaSearchType(value, path, errors) {
195
+ if (value === undefined)
196
+ return undefined;
197
+ const searchType = validateString(value, path, errors)?.trim();
198
+ if (searchType === undefined)
199
+ return undefined;
200
+ if (!EXA_SEARCH_TYPES.includes(/** @type {ExaSearchType} */ (searchType))) {
201
+ errors.push(`${path} must be one of ${EXA_SEARCH_TYPES.join(", ")}.`);
202
+ return undefined;
203
+ }
204
+ return /** @type {ExaSearchType} */ (searchType);
205
+ }
185
206
  /**
186
207
  * @param {unknown} value
187
208
  * @param {string} path
@@ -251,6 +272,9 @@ export function validateAgentBrowserConfig(value, path, errors, warnings) {
251
272
  const preferredProvider = validateWebSearchProvider(value.webSearch.preferredProvider, `${path}.webSearch.preferredProvider`, errors);
252
273
  if (preferredProvider)
253
274
  webSearch.preferredProvider = preferredProvider;
275
+ const defaultSearchType = validateExaSearchType(value.webSearch.defaultSearchType, `${path}.webSearch.defaultSearchType`, errors);
276
+ if (defaultSearchType)
277
+ webSearch.defaultSearchType = defaultSearchType;
254
278
  for (const provider of WEB_SEARCH_PROVIDERS) {
255
279
  const descriptor = getWebSearchProviderDescriptor(provider);
256
280
  const apiKey = validateString(value.webSearch[descriptor.configKey], `${path}.webSearch.${descriptor.configKey}`, errors);
@@ -2,7 +2,7 @@ import { exec as execCallback } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { promisify } from "node:util";
4
4
  import { SECRET_COMMAND_TIMEOUT_MS, buildAgentBrowserConfigState, getAgentBrowserConfigPaths, getWebSearchCredentialSource, getWebSearchProviderOrder, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, } from "./config-policy.js";
5
- export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isPlaintextCredentialValue, isProjectSafeCredentialValueForProvider, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
5
+ export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isPlaintextCredentialValue, isProjectSafeCredentialValueForProvider, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
6
6
  const exec = promisify(execCallback);
7
7
  async function readConfigLayer(path, scope, errors, warnings) {
8
8
  let raw;
@@ -213,15 +213,6 @@ export function compileAgentBrowserJob(input) {
213
213
  }
214
214
  return { compiled: { args: failFast ? ["batch", "--bail"] : ["batch"], failFast, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
215
215
  }
216
- export function isHttpOrHttpsUrl(url) {
217
- try {
218
- const protocol = new URL(url).protocol;
219
- return protocol === "http:" || protocol === "https:";
220
- }
221
- catch {
222
- return false;
223
- }
224
- }
225
216
  function describeQaChecksRun(checks) {
226
217
  const parts = [`load:${checks.loadState}`];
227
218
  if (checks.expectedText.length > 0)
@@ -266,7 +257,7 @@ export function buildQaCompactPassText(options) {
266
257
  lines.push(`Page: ${pageParts.join(" — ")}`);
267
258
  lines.push(`Checks run: ${describeQaChecksRun(options.checks)} (${options.batchStepCount} batch step${options.batchStepCount === 1 ? "" : "s"})`);
268
259
  if (options.checks.diagnosticsResetAtStart && (options.checks.checkNetwork || options.checks.checkConsole || options.checks.checkErrors)) {
269
- lines.push("Diagnostic reset: URL QA cleared enabled network/console/page-error buffers before opening the target; reset rows in details.batchSteps are not counted as current-page failures.");
260
+ lines.push("Diagnostic isolation: URL QA clears enabled network/console buffers, then snapshots any page-error residue before opening the target. Only unchanged residue is ignored because upstream page-error clear is not reliable.");
270
261
  }
271
262
  if (options.checks.attached && !options.checks.diagnosticsResetAtStart && (options.checks.checkNetwork || options.checks.checkConsole || options.checks.checkErrors)) {
272
263
  lines.push("Attached diagnostics: existing upstream session console/network/error buffers were preserved; rows may include events from before qa.attached started.");
@@ -280,6 +271,19 @@ export function buildQaCompactPassText(options) {
280
271
  lines.push("Full diagnostic matrix: see details.qaPreset and details.batchSteps.");
281
272
  return lines.join("\n");
282
273
  }
274
+ export function buildQaCompactFailureText(options) {
275
+ const lines = [options.qaPreset.summary];
276
+ const pageParts = [options.page?.title, options.page?.url].filter((part) => typeof part === "string" && part.length > 0);
277
+ if (pageParts.length > 0)
278
+ lines.push(`Page: ${pageParts.join(" — ")}`);
279
+ if (options.qaPreset.failedChecks.length > 0)
280
+ lines.push("Failed checks:", ...options.qaPreset.failedChecks.map((failure) => `- ${failure}`));
281
+ if (options.qaPreset.warnings.length > 0)
282
+ lines.push("Warnings:", ...options.qaPreset.warnings.map((warning) => `- ${warning}`));
283
+ lines.push(`Checks run: ${describeQaChecksRun(options.checks)} (${options.batchStepCount} batch step${options.batchStepCount === 1 ? "" : "s"})`);
284
+ lines.push("Full diagnostic matrix: see details.qaPreset and details.batchSteps.");
285
+ return lines.join("\n");
286
+ }
283
287
  const QA_VISIBLE_TEXT_TIMEOUT_MS = 5_000;
284
288
  function formatQaExpectedTextPreview(text) {
285
289
  return JSON.stringify(text.length > 80 ? `${text.slice(0, 77)}...` : text);
@@ -348,6 +352,34 @@ function extractQaTextAssertionResultText(item) {
348
352
  }
349
353
  return undefined;
350
354
  }
355
+ function qaErrorSignature(error) {
356
+ if (typeof error === "string")
357
+ return error;
358
+ try {
359
+ return JSON.stringify(error);
360
+ }
361
+ catch {
362
+ return String(error);
363
+ }
364
+ }
365
+ function subtractQaBaselineErrors(errors, baselineErrors) {
366
+ const baselineCounts = new Map();
367
+ for (const error of baselineErrors) {
368
+ const signature = qaErrorSignature(error);
369
+ baselineCounts.set(signature, (baselineCounts.get(signature) ?? 0) + 1);
370
+ }
371
+ let ignoredCount = 0;
372
+ const novelErrors = errors.filter((error) => {
373
+ const signature = qaErrorSignature(error);
374
+ const count = baselineCounts.get(signature) ?? 0;
375
+ if (count === 0)
376
+ return true;
377
+ baselineCounts.set(signature, count - 1);
378
+ ignoredCount += 1;
379
+ return false;
380
+ });
381
+ return { ignoredCount, novelErrors };
382
+ }
351
383
  function isDiagnosticResetCommand(item) {
352
384
  const command = item.command;
353
385
  if (!Array.isArray(command) || !command.every((token) => typeof token === "string"))
@@ -372,17 +404,29 @@ export function analyzeQaPresetResults(data, compiled) {
372
404
  return undefined;
373
405
  const failedChecks = [];
374
406
  const warnings = [];
375
- for (const item of items) {
407
+ const baselineErrorIndex = compiled?.checks.diagnosticsResetAtStart && compiled.checks.checkErrors
408
+ ? compiled.steps.findIndex((step) => step.generatedFrom === "qa.errorBaselineAfterClear")
409
+ : -1;
410
+ const baselineErrorItem = baselineErrorIndex >= 0 ? items[baselineErrorIndex] : undefined;
411
+ const baselineErrorResult = isRecord(baselineErrorItem?.result) ? baselineErrorItem.result : undefined;
412
+ const baselineErrors = Array.isArray(baselineErrorResult?.errors) ? baselineErrorResult.errors : [];
413
+ for (const [index, item] of items.entries()) {
376
414
  if (item.success === false) {
377
415
  failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
378
416
  }
417
+ if (index === baselineErrorIndex)
418
+ continue;
379
419
  const result = isRecord(item.result) ? item.result : undefined;
380
420
  const commandName = getCommandNameFromBatchItem(item);
381
421
  if (compiled?.checks.diagnosticsResetAtStart && isDiagnosticResetCommand(item)) {
382
422
  continue;
383
423
  }
384
424
  if (commandName === "errors" && Array.isArray(result?.errors) && result.errors.length > 0) {
385
- failedChecks.push(`${result.errors.length} page error(s)`);
425
+ const { ignoredCount, novelErrors } = subtractQaBaselineErrors(result.errors, baselineErrors);
426
+ if (novelErrors.length > 0)
427
+ failedChecks.push(`${novelErrors.length} page error(s)`);
428
+ if (ignoredCount > 0)
429
+ warnings.push(`${ignoredCount} post-clear page error residue row(s) ignored as unchanged`);
386
430
  }
387
431
  if (commandName === "console" && Array.isArray(result?.messages)) {
388
432
  const errorCount = result.messages.filter((message) => isRecord(message) && /error/i.test(String(message.type ?? message.level ?? ""))).length;
@@ -477,11 +521,15 @@ export function compileAgentBrowserQaPreset(input) {
477
521
  steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
478
522
  if (diagnosticsResetAtStart && checkConsole)
479
523
  steps.push({ action: "wait", args: ["console", "--clear"] });
480
- if (diagnosticsResetAtStart && checkErrors)
524
+ if (diagnosticsResetAtStart && checkErrors) {
481
525
  steps.push({ action: "wait", args: ["errors", "--clear"] });
526
+ steps.push({ action: "wait", args: ["errors"], generatedFrom: "qa.errorBaselineAfterClear" });
527
+ }
482
528
  if (!attached && normalizedUrl)
483
529
  steps.push({ action: "open", args: ["open", normalizedUrl] });
484
530
  steps.push({ action: "wait", args: ["wait", "--load", loadState] });
531
+ if (checkConsole || checkErrors)
532
+ steps.push({ action: "wait", args: ["wait", "150"], generatedFrom: "qa.diagnosticSettle" });
485
533
  for (const text of expectedText) {
486
534
  steps.push({ action: "assertText", args: ["wait", "--fn", buildQaVisibleTextPredicate(text), "--timeout", String(QA_VISIBLE_TEXT_TIMEOUT_MS)] });
487
535
  }
@@ -261,8 +261,8 @@ export function compileAgentBrowserNetworkSourceLookup(input) {
261
261
  return { error: "networkSourceLookup.filter must be a non-empty string when provided." };
262
262
  if (requestId !== undefined && (typeof requestId !== "string" || requestId.trim().length === 0))
263
263
  return { error: "networkSourceLookup.requestId must be a non-empty string when provided." };
264
- if (namespace !== undefined && (typeof namespace !== "string" || namespace.trim().length === 0))
265
- return { error: "networkSourceLookup.namespace must be a non-empty string when provided." };
264
+ if (namespace !== undefined && (typeof namespace !== "string" || (namespace !== "" && namespace.trim().length === 0)))
265
+ return { error: "networkSourceLookup.namespace must be a non-empty string or the empty default namespace when provided." };
266
266
  if (session !== undefined && (typeof session !== "string" || session.trim().length === 0))
267
267
  return { error: "networkSourceLookup.session must be a non-empty string when provided." };
268
268
  if (url !== undefined && (typeof url !== "string" || url.trim().length === 0))