pi-ui-extend 1.0.41 → 1.0.45

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 (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -0,0 +1,895 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ const MINIMUM_CLI_VERSION = [1, 9, 0];
7
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
8
+ const DEFAULT_ACTION_TIMEOUT_MS = 10_000;
9
+ const MAX_ACTION_TIMEOUT_MS = 60_000;
10
+ const POLL_MS = 150;
11
+ const SAFE_NAME = /^[A-Za-z0-9._-]+$/;
12
+
13
+ export const CHROME_DEVTOOLS_DRIVER = "chrome-devtools-cli";
14
+ export const CHROME_DEVTOOLS_ONLY_ACTIONS = new Set([
15
+ "snapshotAccessibility",
16
+ "assertNoConsoleErrors",
17
+ "assertConsole",
18
+ "assertNetworkRequest",
19
+ "lighthouse",
20
+ "performanceTrace",
21
+ "heapSummary",
22
+ ]);
23
+
24
+ export const CHROME_DEVTOOLS_SUPPORTED_ACTIONS = new Set([
25
+ "goto",
26
+ "reload",
27
+ "click",
28
+ "doubleClick",
29
+ "hover",
30
+ "fill",
31
+ "press",
32
+ "waitFor",
33
+ "waitForTimeout",
34
+ "assertVisible",
35
+ "assertText",
36
+ "assertURL",
37
+ "screenshot",
38
+ ...CHROME_DEVTOOLS_ONLY_ACTIONS,
39
+ ]);
40
+
41
+ export function chromeDevtoolsRequiredByFlow(flow) {
42
+ return Array.isArray(flow?.steps) && flow.steps.some((step) => CHROME_DEVTOOLS_ONLY_ACTIONS.has(step?.action));
43
+ }
44
+
45
+ export function unsupportedChromeDevtoolsActions(flow) {
46
+ if (!Array.isArray(flow?.steps)) return [];
47
+ return [...new Set(flow.steps
48
+ .map((step) => step?.action)
49
+ .filter((action) => typeof action === "string" && !CHROME_DEVTOOLS_SUPPORTED_ACTIONS.has(action)))];
50
+ }
51
+
52
+ export async function probeChromeDevtoolsProvider(context = {}) {
53
+ const executable = resolveChromeDevtoolsExecutable(context.chromeDevtoolsPath);
54
+ const supportedCapabilities = [
55
+ "navigate", "semanticLocators", "pointerInput", "keyboardInput", "accessibilityTree",
56
+ "deterministicAssertions", "screenshot", "originIsolation", "consoleInspection",
57
+ "networkInspection", "lighthouse", "performanceTrace", "memorySummary", "ownedCleanup",
58
+ ];
59
+ if (!executable) {
60
+ return {
61
+ available: false,
62
+ platformDriver: CHROME_DEVTOOLS_DRIVER,
63
+ supportedCapabilities: [],
64
+ missingCapabilities: ["chromeDevtoolsCli"],
65
+ reason: "Chrome DevTools browser QA requires the chrome-devtools CLI, but it is not available on PATH",
66
+ remediation: "install chrome-devtools-mcp with `npm install -g chrome-devtools-mcp@latest`, verify `chrome-devtools --version`, then rerun UI QA",
67
+ };
68
+ }
69
+
70
+ const version = probeCliVersion(executable);
71
+ if (!version || compareVersions(version.parts, MINIMUM_CLI_VERSION) < 0) {
72
+ return {
73
+ available: false,
74
+ platformDriver: CHROME_DEVTOOLS_DRIVER,
75
+ supportedCapabilities: [],
76
+ missingCapabilities: ["chromeDevtoolsCli>=1.9.0"],
77
+ reason: version
78
+ ? `Chrome DevTools browser QA requires chrome-devtools-mcp >= 1.9.0; found ${version.raw}`
79
+ : "Chrome DevTools browser QA could not determine the installed chrome-devtools CLI version",
80
+ remediation: "upgrade with `npm install -g chrome-devtools-mcp@latest`, verify `chrome-devtools --version`, then rerun UI QA",
81
+ };
82
+ }
83
+
84
+ const unsupported = unsupportedChromeDevtoolsActions(context.flow);
85
+ if (unsupported.length > 0) {
86
+ return {
87
+ available: false,
88
+ platformDriver: CHROME_DEVTOOLS_DRIVER,
89
+ supportedCapabilities,
90
+ missingCapabilities: unsupported.map((action) => `action:${action}`),
91
+ reason: `the Chrome DevTools provider does not support these requested flow actions: ${unsupported.join(", ")}`,
92
+ remediation: "use browserDriver `playwright` for Playwright-only actions, or split DevTools diagnostics into a separate Chrome DevTools UI-QA flow",
93
+ };
94
+ }
95
+ if (context.flow?.environment !== undefined) {
96
+ return {
97
+ available: false,
98
+ platformDriver: CHROME_DEVTOOLS_DRIVER,
99
+ supportedCapabilities,
100
+ missingCapabilities: ["deterministicBrowserEnvironment"],
101
+ reason: "the Chrome DevTools provider does not currently reproduce the Playwright locale/timezone/reduced-motion environment contract",
102
+ remediation: "use browserDriver `playwright` when a deterministic browser environment is part of the acceptance surface, or split DevTools diagnostics into a separate flow without top-level environment overrides",
103
+ };
104
+ }
105
+
106
+ return {
107
+ available: true,
108
+ platformDriver: CHROME_DEVTOOLS_DRIVER,
109
+ supportedCapabilities,
110
+ missingCapabilities: ["video", "trustedAuthentication", "downloads"],
111
+ reason: `Chrome DevTools CLI ${version.raw} is available with an isolated per-run daemon, exact-origin network restrictions, redacted network headers, and JavaScript evaluation disabled`,
112
+ remediation: undefined,
113
+ executable,
114
+ };
115
+ }
116
+
117
+ export function chromeDevtoolsStartArgs({ sessionId, evidenceDir, allowedOrigins, devtools = {} }) {
118
+ const args = [
119
+ "start",
120
+ "--sessionId", sessionId,
121
+ "--redactNetworkHeaders=true",
122
+ "--usageStatistics=false",
123
+ "--performanceCrux=false",
124
+ "--pageIdRouting=true",
125
+ "--javascriptEvaluation=false",
126
+ "--categoryExtensions=false",
127
+ "--categoryPwa=false",
128
+ "--categoryExperimentalThirdParty=false",
129
+ "--categoryExperimentalWebmcp=false",
130
+ "--experimentalVision=false",
131
+ `--workspace=${evidenceDir}`,
132
+ ];
133
+ for (const origin of allowedOrigins) args.push(`--allowedUrlPattern=${origin}/*`);
134
+ if (devtools.browserUrl) args.push(`--browserUrl=${devtools.browserUrl}`);
135
+ else {
136
+ args.push("--isolated=true");
137
+ args.push(`--headless=${devtools.headless !== false}`);
138
+ }
139
+ return args;
140
+ }
141
+
142
+ export async function runChromeDevtoolsProvider(context, config) {
143
+ const probe = await probeChromeDevtoolsProvider({ ...context, flow: config.originalFlow });
144
+ if (!probe.available) return blocked(probe.reason, probe.remediation);
145
+ if (config.profile) {
146
+ return blocked(
147
+ "Chrome DevTools browser QA does not consume trusted QA credential profiles",
148
+ "use browserDriver `playwright` for target.profile authentication; Chrome DevTools may only reuse a live browser session when the task explicitly authorizes devtools.browserUrl plus reuseExistingBrowserSession",
149
+ );
150
+ }
151
+
152
+ const executable = probe.executable;
153
+ // chrome-devtools-mcp intentionally restricts daemon session ids to hex and
154
+ // hyphens because the value is embedded in its private socket/runtime paths.
155
+ // A 128-bit random hex id gives this run an isolated daemon namespace without
156
+ // colliding with the user's default or other QA sessions.
157
+ const sessionId = randomBytes(16).toString("hex");
158
+ const environment = chromeDevtoolsEnvironment();
159
+ const assertions = [];
160
+ const observations = [];
161
+ const artifacts = emptyArtifacts();
162
+ let pageId;
163
+ let daemonStarted = false;
164
+ let failure;
165
+
166
+ try {
167
+ const startArgs = chromeDevtoolsStartArgs({
168
+ sessionId,
169
+ evidenceDir: context.evidenceDir,
170
+ allowedOrigins: config.allowedOrigins,
171
+ devtools: config.devtools,
172
+ });
173
+ const start = await runCliProcess(executable, startArgs, {
174
+ cwd: context.projectRoot,
175
+ env: environment,
176
+ timeoutMs: boundedRemaining(context, 20_000),
177
+ });
178
+ if (start.code !== 0) {
179
+ // `start` normally either succeeds atomically or leaves no daemon, but
180
+ // stop this private session best-effort in case startup failed after the
181
+ // daemon socket/PID had already been created.
182
+ await runCliProcess(executable, ["stop", "--sessionId", sessionId], {
183
+ cwd: context.projectRoot,
184
+ env: environment,
185
+ timeoutMs: 5_000,
186
+ }).catch(() => {});
187
+ return blocked(
188
+ "Chrome DevTools CLI could not start its isolated UI-QA daemon/browser with the required safety policy",
189
+ `verify Chrome 149+ is installed and that chrome-devtools-mcp can start with allowedUrlPattern; ${safeCliFailure(start)}`,
190
+ );
191
+ }
192
+ daemonStarted = true;
193
+ observations.push({
194
+ action: "browserProvider",
195
+ provider: CHROME_DEVTOOLS_DRIVER,
196
+ isolatedDaemon: true,
197
+ attachedExistingChrome: Boolean(config.devtools.browserUrl),
198
+ reuseExistingBrowserSession: Boolean(config.devtools.reuseExistingBrowserSession),
199
+ javascriptEvaluation: false,
200
+ networkHeadersRedacted: true,
201
+ allowedOriginCount: config.allowedOrigins.length,
202
+ });
203
+
204
+ const page = await invokeCliJson(executable, sessionId, "new_page", [
205
+ config.baseUrl,
206
+ ...(config.devtools.browserUrl && !config.devtools.reuseExistingBrowserSession
207
+ ? ["--isolatedContext", sessionId]
208
+ : []),
209
+ "--timeout", String(Math.min(30_000, boundedRemaining(context, 30_000))),
210
+ ], context, environment);
211
+ pageId = selectedPageId(page, config.baseUrl);
212
+ if (!Number.isInteger(pageId)) throw new Error("Chrome DevTools did not return a task-owned page id");
213
+ const viewport = validateDevtoolsViewport(config.flow.viewport);
214
+ if (viewport) {
215
+ await invokeCliJson(executable, sessionId, "resize_page", [String(pageId), String(viewport.width), String(viewport.height)], context, environment);
216
+ observations.push({ action: "viewport", width: viewport.width, height: viewport.height });
217
+ }
218
+ await assertPageOriginAllowed(executable, sessionId, pageId, config.allowedOrigins, context, environment);
219
+ observations.push({ action: "newPage", pageId, isolatedContext: !config.devtools.reuseExistingBrowserSession });
220
+
221
+ for (let index = 0; index < config.originalFlow.steps.length; index += 1) {
222
+ const step = validateDevtoolsStep(config.originalFlow.steps[index], index);
223
+ context.progress("browser_devtools_action_started", { index, action: step.action });
224
+ try {
225
+ await executeStep({ executable, sessionId, pageId, step, index, context, config, environment, assertions, observations, artifacts });
226
+ if (["goto", "reload", "click", "doubleClick", "fill", "press"].includes(step.action)) {
227
+ await assertPageOriginAllowed(executable, sessionId, pageId, config.allowedOrigins, context, environment);
228
+ }
229
+ context.progress("browser_devtools_action_finished", { index, action: step.action });
230
+ } catch (error) {
231
+ failure = new Error(`step ${index + 1} (${step.action}) failed: ${safeReason(error)}`);
232
+ break;
233
+ }
234
+ }
235
+
236
+ if (!failure) {
237
+ await captureAccessibility({ executable, sessionId, pageId, context, environment, artifacts, name: "final", verbose: false }).catch((error) => {
238
+ observations.push({ action: "finalSnapshot", status: "unavailable", reason: safeReason(error) });
239
+ });
240
+ }
241
+ } catch (error) {
242
+ failure = error;
243
+ } finally {
244
+ if (failure && Number.isInteger(pageId)) {
245
+ await captureAccessibility({ executable, sessionId, pageId, context, environment, artifacts, name: "failure", verbose: false }).catch(() => {});
246
+ await captureScreenshot({ executable, sessionId, pageId, context, environment, artifacts, name: "failure", fullPage: true }).catch(() => {});
247
+ }
248
+ if (Number.isInteger(pageId)) {
249
+ await invokeCliJson(executable, sessionId, "close_page", [String(pageId)], context, environment, 5_000).catch(() => {});
250
+ }
251
+ if (daemonStarted) {
252
+ await runCliProcess(executable, ["stop", "--sessionId", sessionId], {
253
+ cwd: context.projectRoot,
254
+ env: environment,
255
+ timeoutMs: 8_000,
256
+ }).catch(() => {});
257
+ }
258
+ }
259
+
260
+ return {
261
+ status: failure ? "FAILED" : "PASSED",
262
+ ...(failure ? { reason: safeReason(failure) } : {}),
263
+ assertions,
264
+ observations,
265
+ artifacts,
266
+ };
267
+ }
268
+
269
+ async function executeStep(options) {
270
+ const { executable, sessionId, pageId, step, index, context, config, environment, assertions, observations, artifacts } = options;
271
+ const timeoutMs = boundedActionTimeout(step.timeoutMs ?? config.flow.timeoutMs, context);
272
+ switch (step.action) {
273
+ case "goto": {
274
+ const target = typeof step.url === "string" ? step.url : step.path;
275
+ if (typeof target !== "string") throw new Error("goto requires url or path");
276
+ const url = new URL(target, config.baseUrl);
277
+ assertAllowedUrl(url, config.allowedOrigins, "goto");
278
+ await invokeCliJson(executable, sessionId, "navigate_page", [String(pageId), "--type", "url", "--url", url.href, "--timeout", String(timeoutMs)], context, environment);
279
+ observations.push({ action: "goto", origin: url.origin, path: url.pathname });
280
+ return;
281
+ }
282
+ case "reload":
283
+ await invokeCliJson(executable, sessionId, "navigate_page", [String(pageId), "--type", "reload", "--timeout", String(timeoutMs)], context, environment);
284
+ observations.push({ action: "reload" });
285
+ return;
286
+ case "click":
287
+ case "doubleClick":
288
+ case "hover":
289
+ case "fill":
290
+ case "press": {
291
+ const snapshot = await takeSnapshot(executable, sessionId, pageId, context, environment);
292
+ const node = requireLocatorNode(snapshot, step.locator);
293
+ if (step.action === "click" || step.action === "doubleClick") {
294
+ await invokeCliJson(executable, sessionId, "click", [String(pageId), node.id, ...(step.action === "doubleClick" ? ["--dblClick", "true"] : [])], context, environment);
295
+ } else if (step.action === "hover") {
296
+ await invokeCliJson(executable, sessionId, "hover", [String(pageId), node.id], context, environment);
297
+ } else if (step.action === "fill") {
298
+ await invokeCliJson(executable, sessionId, "fill", [String(pageId), node.id, step.value], context, environment);
299
+ } else {
300
+ await invokeCliJson(executable, sessionId, "click", [String(pageId), node.id], context, environment);
301
+ await invokeCliJson(executable, sessionId, "press_key", [String(pageId), step.key], context, environment);
302
+ }
303
+ observations.push({ action: step.action, locator: publicLocator(step.locator) });
304
+ return;
305
+ }
306
+ case "waitFor": {
307
+ const state = step.state ?? "visible";
308
+ if (state !== "visible" && state !== "hidden") throw new Error("Chrome DevTools waitFor supports only visible or hidden state");
309
+ await retryUntil(async () => {
310
+ const snapshot = await takeSnapshot(executable, sessionId, pageId, context, environment);
311
+ const count = findLocatorNodes(snapshot, step.locator).length;
312
+ return state === "visible" ? count === 1 : count === 0;
313
+ }, timeoutMs, `locator did not become ${state}`);
314
+ observations.push({ action: "waitFor", state, locator: publicLocator(step.locator) });
315
+ return;
316
+ }
317
+ case "waitForTimeout":
318
+ await sleep(step.timeoutMs);
319
+ observations.push({ action: "waitForTimeout", durationMs: step.timeoutMs });
320
+ return;
321
+ case "assertVisible": {
322
+ await retryUntil(async () => {
323
+ const snapshot = await takeSnapshot(executable, sessionId, pageId, context, environment);
324
+ return findLocatorNodes(snapshot, step.locator).length === 1;
325
+ }, timeoutMs, "expected locator to be visible and unambiguous");
326
+ assertions.push({ action: step.action, locator: publicLocator(step.locator), passed: true });
327
+ return;
328
+ }
329
+ case "assertText": {
330
+ const matcher = stringMatcher(step, "assertText");
331
+ await retryUntil(async () => {
332
+ const snapshot = await takeSnapshot(executable, sessionId, pageId, context, environment);
333
+ const nodes = findLocatorNodes(snapshot, step.locator);
334
+ return nodes.length === 1 && matcher.matches(nodeText(nodes[0]));
335
+ }, timeoutMs, "text assertion failed");
336
+ assertions.push({ action: step.action, locator: publicLocator(step.locator), expected: matcher.publicExpected, passed: true });
337
+ return;
338
+ }
339
+ case "assertURL": {
340
+ const matcher = stringMatcher(step, "assertURL");
341
+ await retryUntil(async () => matcher.matches((await pageRecord(executable, sessionId, pageId, context, environment)).url), timeoutMs, "URL assertion failed");
342
+ assertions.push({ action: step.action, expected: matcher.publicExpected, passed: true });
343
+ return;
344
+ }
345
+ case "screenshot":
346
+ await captureScreenshot({ executable, sessionId, pageId, context, environment, artifacts, name: evidenceName(step.name, index, "screenshot"), fullPage: step.fullPage !== false });
347
+ return;
348
+ case "snapshotAccessibility":
349
+ await captureAccessibility({ executable, sessionId, pageId, context, environment, artifacts, name: evidenceName(step.name, index, "snapshot"), verbose: step.verbose === true });
350
+ return;
351
+ case "assertNoConsoleErrors": {
352
+ const data = await invokeCliJson(executable, sessionId, "list_console_messages", [String(pageId), "--types", "error", "--pageSize", "200"], context, environment);
353
+ const count = Array.isArray(data.consoleMessages) ? data.consoleMessages.length : 0;
354
+ const passed = count === 0;
355
+ assertions.push({ action: step.action, expected: { consoleErrors: 0 }, actual: { consoleErrors: count }, passed });
356
+ if (!passed) throw new Error("console contains error messages");
357
+ return;
358
+ }
359
+ case "assertConsole": {
360
+ const matcher = stringMatcher(step, "assertConsole");
361
+ await retryUntil(async () => {
362
+ const args = [String(pageId), "--pageSize", "200"];
363
+ if (step.type) args.push("--types", step.type);
364
+ const data = await invokeCliJson(executable, sessionId, "list_console_messages", args, context, environment);
365
+ return (data.consoleMessages ?? []).some((message) => matcher.matches(String(message?.text ?? "")));
366
+ }, timeoutMs, "console assertion failed");
367
+ assertions.push({ action: step.action, ...(step.type ? { type: step.type } : {}), expected: matcher.publicExpected, passed: true });
368
+ return;
369
+ }
370
+ case "assertNetworkRequest": {
371
+ await retryUntil(async () => {
372
+ const data = await invokeCliJson(executable, sessionId, "list_network_requests", [String(pageId), "--pageSize", "200"], context, environment);
373
+ return (data.networkRequests ?? []).some((request) => networkRequestMatches(request, step, config.allowedOrigins));
374
+ }, timeoutMs, "network request assertion failed");
375
+ assertions.push({
376
+ action: step.action,
377
+ expected: { path: step.path, method: step.method, status: step.status, ...(step.origin ? { origin: step.origin } : {}) },
378
+ passed: true,
379
+ });
380
+ return;
381
+ }
382
+ case "lighthouse": {
383
+ const data = await invokeCliJson(executable, sessionId, "lighthouse_audit", [
384
+ String(pageId),
385
+ ...(step.mode ? ["--mode", step.mode] : []),
386
+ ...(step.device ? ["--device", step.device] : []),
387
+ ], context, environment, Math.min(60_000, boundedRemaining(context, 60_000)));
388
+ const summary = sanitizeLighthouseSummary(data.lighthouseResult?.summary);
389
+ if (!summary) throw new Error("Lighthouse did not return a summary");
390
+ const name = `${evidenceName(step.name, index, "lighthouse")}.json`;
391
+ writeJsonArtifact(context, artifacts, "observations", name, summary, "lighthouse summary");
392
+ observations.push({ action: "lighthouse", scores: summary.scores });
393
+ return;
394
+ }
395
+ case "performanceTrace": {
396
+ const base = evidenceName(step.name, index, "performance-trace");
397
+ const rawPath = path.join(context.evidenceDir, `${base}.raw.json`);
398
+ try {
399
+ const data = await invokeCliJson(executable, sessionId, "performance_start_trace", [
400
+ String(pageId),
401
+ "--reload", String(step.reload !== false),
402
+ "--autoStop", "true",
403
+ "--filePath", rawPath,
404
+ ], context, environment, Math.min(30_000, boundedRemaining(context, 30_000)));
405
+ const summary = sanitizePerformanceTrace(data);
406
+ const summaryName = `${base}.json`;
407
+ writeJsonArtifact(context, artifacts, "traces", summaryName, summary, "sanitized performance trace summary");
408
+ observations.push({ action: "performanceTrace", rawTraceRetained: false, insightCount: summary.insightNames.length, metrics: summary.metrics });
409
+ } finally {
410
+ // Raw Chrome trace data can contain URLs, query strings, network details,
411
+ // call frames, and response metadata. It is an implementation scratch
412
+ // artifact only; publish the bounded sanitized summary above instead.
413
+ fs.rmSync(rawPath, { force: true });
414
+ }
415
+ return;
416
+ }
417
+ case "heapSummary": {
418
+ const base = evidenceName(step.name, index, "heap-summary");
419
+ const rawName = `${base}.heapsnapshot`;
420
+ const rawPath = path.join(context.evidenceDir, rawName);
421
+ try {
422
+ await invokeCliJson(executable, sessionId, "take_heapsnapshot", [String(pageId), rawPath], context, environment, Math.min(60_000, boundedRemaining(context, 60_000)));
423
+ const data = await invokeCliJson(executable, sessionId, "get_heapsnapshot_summary", [rawPath], context, environment, Math.min(60_000, boundedRemaining(context, 60_000)));
424
+ const safeSummary = sanitizeHeapSummary(data.heapSnapshot);
425
+ const summaryName = `${base}.json`;
426
+ writeJsonArtifact(context, artifacts, "observations", summaryName, safeSummary, "heap summary");
427
+ observations.push({ action: "heapSummary", rawSnapshotRetained: false });
428
+ } finally {
429
+ await invokeCliJson(executable, sessionId, "close_heapsnapshot", [rawPath], context, environment, 5_000).catch(() => {});
430
+ fs.rmSync(rawPath, { force: true });
431
+ }
432
+ return;
433
+ }
434
+ default:
435
+ throw new Error(`unsupported Chrome DevTools browser action: ${step.action}`);
436
+ }
437
+ }
438
+
439
+ function validateDevtoolsStep(value, index) {
440
+ if (!isObject(value) || typeof value.action !== "string" || !CHROME_DEVTOOLS_SUPPORTED_ACTIONS.has(value.action)) {
441
+ throw new Error(`Chrome DevTools step ${index + 1} uses an unsupported action`);
442
+ }
443
+ const allowed = {
444
+ goto: ["action", "url", "path", "timeoutMs"],
445
+ reload: ["action", "timeoutMs"],
446
+ click: ["action", "locator", "timeoutMs"],
447
+ doubleClick: ["action", "locator", "timeoutMs"],
448
+ hover: ["action", "locator", "timeoutMs"],
449
+ fill: ["action", "locator", "value", "timeoutMs"],
450
+ press: ["action", "locator", "key", "timeoutMs"],
451
+ waitFor: ["action", "locator", "state", "timeoutMs"],
452
+ waitForTimeout: ["action", "timeoutMs"],
453
+ assertVisible: ["action", "locator", "timeoutMs"],
454
+ assertText: ["action", "locator", "equals", "includes", "timeoutMs"],
455
+ assertURL: ["action", "equals", "includes", "timeoutMs"],
456
+ screenshot: ["action", "name", "fullPage"],
457
+ snapshotAccessibility: ["action", "name", "verbose"],
458
+ assertNoConsoleErrors: ["action"],
459
+ assertConsole: ["action", "type", "equals", "includes", "timeoutMs"],
460
+ assertNetworkRequest: ["action", "path", "method", "status", "origin", "timeoutMs"],
461
+ lighthouse: ["action", "name", "mode", "device"],
462
+ performanceTrace: ["action", "name", "reload"],
463
+ heapSummary: ["action", "name"],
464
+ }[value.action];
465
+ assertKnownFields(value, new Set(allowed), `Chrome DevTools step ${index + 1}`);
466
+
467
+ if (["click", "doubleClick", "hover", "fill", "press", "waitFor", "assertVisible", "assertText"].includes(value.action)) validateLocator(value.locator);
468
+ if (value.action === "fill" && typeof value.value !== "string") throw new Error("fill requires a string value");
469
+ if (value.action === "press" && (typeof value.key !== "string" || value.key.length < 1 || value.key.length > 80)) throw new Error("press requires a bounded key");
470
+ if (value.action === "waitForTimeout" && (!Number.isInteger(value.timeoutMs) || value.timeoutMs < 1 || value.timeoutMs > 5_000)) throw new Error("waitForTimeout must be between 1 and 5000 ms");
471
+ if (value.action === "assertConsole" && value.type !== undefined && !["error", "warning", "info", "log", "debug", "table", "trace"].includes(value.type)) throw new Error("assertConsole.type is unsupported");
472
+ if (value.action === "assertNetworkRequest") {
473
+ if (typeof value.path !== "string" || !value.path.startsWith("/") || value.path.includes("?") || value.path.includes("#") || value.path.length > 2048) throw new Error("assertNetworkRequest.path must be an exact origin-relative path without query or fragment");
474
+ if (typeof value.method !== "string" || !/^[A-Z]{3,12}$/.test(value.method)) throw new Error("assertNetworkRequest.method must be uppercase HTTP method text");
475
+ if (!Number.isInteger(value.status) || value.status < 100 || value.status > 599) throw new Error("assertNetworkRequest.status must be 100..599");
476
+ }
477
+ if (value.action === "lighthouse") {
478
+ if (value.mode !== undefined && !["navigation", "snapshot"].includes(value.mode)) throw new Error("lighthouse.mode is unsupported");
479
+ if (value.device !== undefined && !["mobile", "desktop"].includes(value.device)) throw new Error("lighthouse.device is unsupported");
480
+ }
481
+ if (value.name !== undefined) evidenceName(value.name, index, value.action);
482
+ return value;
483
+ }
484
+
485
+ function validateLocator(value) {
486
+ if (!isObject(value)) throw new Error("Chrome DevTools locator must be an object");
487
+ assertKnownFields(value, new Set(["role", "name", "text", "exact"]), "Chrome DevTools locator");
488
+ if (value.exact !== undefined && typeof value.exact !== "boolean") throw new Error("Chrome DevTools locator.exact must be boolean");
489
+ if (value.role !== undefined) {
490
+ if (typeof value.role !== "string" || value.role.length < 1 || value.role.length > 80) throw new Error("Chrome DevTools locator.role must be bounded text");
491
+ if (value.name !== undefined && (typeof value.name !== "string" || value.name.length > 500)) throw new Error("Chrome DevTools locator.name must be bounded text");
492
+ if (value.text !== undefined) throw new Error("Chrome DevTools locator cannot combine role/name with text");
493
+ return;
494
+ }
495
+ if (typeof value.text !== "string" || value.text.length < 1 || value.text.length > 500) throw new Error("Chrome DevTools locator requires role or text");
496
+ if (value.name !== undefined) throw new Error("Chrome DevTools locator.name requires role");
497
+ }
498
+
499
+ function validateDevtoolsViewport(value) {
500
+ if (value === undefined) return undefined;
501
+ if (!isObject(value)) throw new Error("viewport must be an object");
502
+ assertKnownFields(value, new Set(["width", "height"]), "viewport");
503
+ if (!Number.isInteger(value.width) || value.width < 320 || value.width > 3840) throw new Error("viewport.width must be 320..3840");
504
+ if (!Number.isInteger(value.height) || value.height < 240 || value.height > 2160) throw new Error("viewport.height must be 240..2160");
505
+ return { width: value.width, height: value.height };
506
+ }
507
+
508
+ async function takeSnapshot(executable, sessionId, pageId, context, environment, verbose = false) {
509
+ const data = await invokeCliJson(executable, sessionId, "take_snapshot", [String(pageId), "--verbose", String(verbose)], context, environment);
510
+ if (!isObject(data.snapshot)) throw new Error("Chrome DevTools accessibility snapshot is unavailable");
511
+ return data.snapshot;
512
+ }
513
+
514
+ async function captureAccessibility(options) {
515
+ const { executable, sessionId, pageId, context, environment, artifacts, name, verbose } = options;
516
+ const snapshot = await takeSnapshot(executable, sessionId, pageId, context, environment, verbose);
517
+ const fileName = `${name}.accessibility.json`;
518
+ writeJsonArtifact(context, artifacts, "accessibilitySnapshots", fileName, snapshot, "Chrome DevTools accessibility snapshot");
519
+ }
520
+
521
+ async function captureScreenshot(options) {
522
+ const { executable, sessionId, pageId, context, environment, artifacts, name, fullPage } = options;
523
+ const fileName = `${name}.png`;
524
+ const destination = path.join(context.evidenceDir, fileName);
525
+ await invokeCliJson(executable, sessionId, "take_screenshot", [String(pageId), "--format", "png", "--fullPage", String(fullPage), "--filePath", destination], context, environment);
526
+ if (!fs.existsSync(destination) || fs.statSync(destination).size === 0) throw new Error("Chrome DevTools screenshot was not written");
527
+ artifacts.screenshots.push({ ...context.artifact(fileName, "Chrome DevTools screenshot"), format: "png" });
528
+ }
529
+
530
+ function writeJsonArtifact(context, artifacts, bucket, fileName, value, kind) {
531
+ const destination = path.join(context.evidenceDir, fileName);
532
+ fs.writeFileSync(destination, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
533
+ if (process.platform !== "win32") fs.chmodSync(destination, 0o600);
534
+ artifacts[bucket].push({ ...context.artifact(fileName, kind), format: "json" });
535
+ }
536
+
537
+ function findLocatorNodes(snapshot, locator) {
538
+ validateLocator(locator);
539
+ const nodes = flattenSnapshot(snapshot);
540
+ const exact = locator.exact === true;
541
+ if (locator.role) {
542
+ return nodes.filter((node) => {
543
+ if (String(node.role ?? "").toLowerCase() !== locator.role.toLowerCase()) return false;
544
+ if (locator.name === undefined) return true;
545
+ return textMatches(String(node.name ?? ""), locator.name, exact);
546
+ });
547
+ }
548
+ return nodes.filter((node) => textMatches(nodeText(node), locator.text, exact));
549
+ }
550
+
551
+ function requireLocatorNode(snapshot, locator) {
552
+ const matches = findLocatorNodes(snapshot, locator);
553
+ if (matches.length === 0) throw new Error("Chrome DevTools locator matched no accessibility node");
554
+ if (matches.length > 1) throw new Error(`Chrome DevTools locator is ambiguous (${matches.length} accessibility nodes)`);
555
+ if (typeof matches[0].id !== "string" || matches[0].id.length === 0) throw new Error("Chrome DevTools accessibility node has no UID");
556
+ return matches[0];
557
+ }
558
+
559
+ function flattenSnapshot(root) {
560
+ const result = [];
561
+ const stack = [root];
562
+ while (stack.length > 0) {
563
+ const node = stack.pop();
564
+ if (!isObject(node)) continue;
565
+ result.push(node);
566
+ if (Array.isArray(node.children)) for (let index = node.children.length - 1; index >= 0; index -= 1) stack.push(node.children[index]);
567
+ }
568
+ return result;
569
+ }
570
+
571
+ function nodeText(node) {
572
+ return [node.name, node.value, node.description, node.placeholder]
573
+ .filter((value) => typeof value === "string" && value.length > 0)
574
+ .join(" ");
575
+ }
576
+
577
+ function textMatches(actual, expected, exact) {
578
+ return exact ? actual === expected : actual.includes(expected);
579
+ }
580
+
581
+ function publicLocator(locator) {
582
+ return {
583
+ ...(locator.role ? { role: locator.role } : {}),
584
+ ...(locator.name !== undefined ? { name: locator.name } : {}),
585
+ ...(locator.text !== undefined ? { text: locator.text } : {}),
586
+ ...(locator.exact === true ? { exact: true } : {}),
587
+ };
588
+ }
589
+
590
+ function stringMatcher(step, label) {
591
+ const hasEquals = typeof step.equals === "string";
592
+ const hasIncludes = typeof step.includes === "string";
593
+ if (hasEquals === hasIncludes) throw new Error(`${label} requires exactly one of equals or includes`);
594
+ const expected = hasEquals ? step.equals : step.includes;
595
+ if (expected.length > 4096) throw new Error(`${label} matcher is too long`);
596
+ return {
597
+ matches: (actual) => hasEquals ? actual === expected : actual.includes(expected),
598
+ publicExpected: hasEquals ? { equals: expected } : { includes: expected },
599
+ };
600
+ }
601
+
602
+ function networkRequestMatches(request, step, allowedOrigins) {
603
+ if (!isObject(request) || typeof request.url !== "string") return false;
604
+ let url;
605
+ try { url = new URL(request.url); } catch { return false; }
606
+ if (!allowedOrigins.includes(url.origin)) return false;
607
+ if (step.origin !== undefined && url.origin !== step.origin) return false;
608
+ const status = typeof request.status === "string" && /^\d{3}$/.test(request.status)
609
+ ? Number(request.status)
610
+ : request.status;
611
+ return url.pathname === step.path && request.method === step.method && status === step.status;
612
+ }
613
+
614
+ function sanitizeLighthouseSummary(summary) {
615
+ if (!isObject(summary)) return undefined;
616
+ return {
617
+ mode: String(summary.mode ?? ""),
618
+ device: String(summary.device ?? ""),
619
+ url: safeHttpUrlString(summary.url),
620
+ scores: Array.isArray(summary.scores)
621
+ ? summary.scores.map((score) => ({ id: String(score?.id ?? ""), title: String(score?.title ?? ""), score: finiteNumberOrNull(score?.score) }))
622
+ : [],
623
+ audits: isObject(summary.audits) ? {
624
+ passed: integerOrZero(summary.audits.passed),
625
+ failed: integerOrZero(summary.audits.failed),
626
+ } : { passed: 0, failed: 0 },
627
+ timingMs: isObject(summary.timing) ? finiteNumberOrNull(summary.timing.total) : null,
628
+ };
629
+ }
630
+
631
+ function sanitizeHeapSummary(value) {
632
+ const source = isObject(value) ? value : {};
633
+ const aggregate = isObject(source.aggregateStats) ? source.aggregateStats : {};
634
+ return {
635
+ stats: isObject(source.stats) ? numericRecord(source.stats) : {},
636
+ aggregateStats: {
637
+ objectCount: integerOrZero(aggregate.objectCount),
638
+ totalSelfSize: finiteNumberOrNull(aggregate.totalSelfSize),
639
+ },
640
+ };
641
+ }
642
+
643
+ function sanitizePerformanceTrace(value) {
644
+ const text = typeof value?.traceSummary === "string" ? value.traceSummary : "";
645
+ const insightNames = Array.isArray(value?.traceInsights)
646
+ ? [...new Set(value.traceInsights
647
+ .map((entry) => String(entry?.insightName ?? ""))
648
+ .filter((name) => /^[A-Za-z0-9._-]{1,80}$/.test(name)))]
649
+ : [];
650
+ const metrics = {};
651
+ const lcp = /(?:^|\n)\s*- LCP:\s*([0-9]+(?:\.[0-9]+)?)\s*ms\b/m.exec(text);
652
+ const cls = /(?:^|\n)\s*- CLS:\s*([0-9]+(?:\.[0-9]+)?)/m.exec(text);
653
+ const cpu = /(?:^|\n)CPU throttling:\s*([0-9]+(?:\.[0-9]+)?)x\b/m.exec(text);
654
+ if (lcp) metrics.lcpMs = Number(lcp[1]);
655
+ if (cls) metrics.cls = Number(cls[1]);
656
+ if (cpu) metrics.cpuThrottlingRate = Number(cpu[1]);
657
+ return { metrics, insightNames };
658
+ }
659
+
660
+ async function assertPageOriginAllowed(executable, sessionId, pageId, allowedOrigins, context, environment) {
661
+ const page = await pageRecord(executable, sessionId, pageId, context, environment);
662
+ const url = new URL(page.url);
663
+ assertAllowedUrl(url, allowedOrigins, "page");
664
+ }
665
+
666
+ async function pageRecord(executable, sessionId, pageId, context, environment) {
667
+ const data = await invokeCliJson(executable, sessionId, "list_pages", [], context, environment);
668
+ const page = Array.isArray(data.pages) ? data.pages.find((entry) => entry?.id === pageId) : undefined;
669
+ if (!page || typeof page.url !== "string") throw new Error(`Chrome DevTools page ${pageId} is no longer available`);
670
+ return page;
671
+ }
672
+
673
+ function selectedPageId(data, targetUrl) {
674
+ if (!Array.isArray(data?.pages)) return undefined;
675
+ const selected = data.pages.find((entry) => entry?.selected && typeof entry?.id === "number");
676
+ if (selected) return selected.id;
677
+ const exact = data.pages.find((entry) => entry?.url === targetUrl && typeof entry?.id === "number");
678
+ return exact?.id;
679
+ }
680
+
681
+ function assertAllowedUrl(url, allowedOrigins, label) {
682
+ if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password || !allowedOrigins.includes(url.origin)) {
683
+ throw new Error(`${label} target is outside the exact allowed origins`);
684
+ }
685
+ }
686
+
687
+ async function invokeCliJson(executable, sessionId, command, args, context, environment, timeoutOverride) {
688
+ const timeoutMs = timeoutOverride ?? boundedRemaining(context, 30_000);
689
+ const execution = await runCliProcess(executable, [command, ...args, "--sessionId", sessionId, "--output-format=json"], {
690
+ cwd: context.projectRoot,
691
+ env: environment,
692
+ timeoutMs,
693
+ });
694
+ if (execution.code !== 0) throw new Error(`chrome-devtools ${command} failed: ${safeCliFailure(execution)}`);
695
+ const text = execution.stdout.trim();
696
+ if (!text) return {};
697
+ try {
698
+ const parsed = JSON.parse(text.split(/\r?\n/).filter(Boolean).pop());
699
+ if (Array.isArray(parsed)) {
700
+ const firstText = parsed.find((entry) => entry?.type === "text" && typeof entry.text === "string");
701
+ if (firstText) {
702
+ try { return JSON.parse(firstText.text); } catch { return { message: firstText.text }; }
703
+ }
704
+ return {};
705
+ }
706
+ return isObject(parsed) ? parsed : {};
707
+ } catch {
708
+ throw new Error(`chrome-devtools ${command} returned invalid JSON`);
709
+ }
710
+ }
711
+
712
+ function runCliProcess(executable, args, options) {
713
+ return new Promise((resolve, reject) => {
714
+ const child = spawn(executable, args, {
715
+ cwd: options.cwd,
716
+ env: options.env,
717
+ stdio: ["ignore", "pipe", "pipe"],
718
+ windowsHide: true,
719
+ });
720
+ let stdout = Buffer.alloc(0);
721
+ let stderr = Buffer.alloc(0);
722
+ let outputTruncated = false;
723
+ let settled = false;
724
+ const append = (current, chunk) => {
725
+ const bytes = Buffer.from(chunk);
726
+ if (current.length >= MAX_OUTPUT_BYTES) {
727
+ outputTruncated = true;
728
+ return current;
729
+ }
730
+ const kept = bytes.subarray(0, MAX_OUTPUT_BYTES - current.length);
731
+ if (kept.length < bytes.length) outputTruncated = true;
732
+ return Buffer.concat([current, kept]);
733
+ };
734
+ child.stdout?.on("data", (chunk) => { stdout = append(stdout, chunk); });
735
+ child.stderr?.on("data", (chunk) => { stderr = append(stderr, chunk); });
736
+ const timer = setTimeout(() => {
737
+ if (settled) return;
738
+ settled = true;
739
+ try { child.kill("SIGKILL"); } catch { /* already exited */ }
740
+ resolve({ code: 124, stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8"), timedOut: true, outputTruncated });
741
+ }, Math.max(100, options.timeoutMs));
742
+ child.once("error", (error) => {
743
+ if (settled) return;
744
+ settled = true;
745
+ clearTimeout(timer);
746
+ reject(error);
747
+ });
748
+ child.once("close", (code) => {
749
+ if (settled) return;
750
+ settled = true;
751
+ clearTimeout(timer);
752
+ resolve({ code: code ?? 1, stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8"), timedOut: false, outputTruncated });
753
+ });
754
+ });
755
+ }
756
+
757
+ function resolveChromeDevtoolsExecutable(explicit) {
758
+ if (typeof explicit === "string" && explicit.length > 0) return fs.existsSync(explicit) ? explicit : undefined;
759
+ const names = process.platform === "win32" ? ["chrome-devtools.exe", "chrome-devtools.cmd", "chrome-devtools"] : ["chrome-devtools"];
760
+ const pathValue = process.env.PATH ?? "";
761
+ const extensions = process.platform === "win32"
762
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
763
+ : [""];
764
+ for (const directory of pathValue.split(path.delimiter).filter(Boolean)) {
765
+ for (const name of names) {
766
+ const base = path.join(directory, name);
767
+ const candidates = process.platform === "win32" && path.extname(name) === ""
768
+ ? [base, ...extensions.map((extension) => `${base}${extension.toLowerCase()}`), ...extensions.map((extension) => `${base}${extension.toUpperCase()}`)]
769
+ : [base];
770
+ for (const candidate of candidates) {
771
+ try {
772
+ if (fs.statSync(candidate).isFile()) return candidate;
773
+ } catch { /* keep searching */ }
774
+ }
775
+ }
776
+ }
777
+ return undefined;
778
+ }
779
+
780
+ function probeCliVersion(executable) {
781
+ const result = spawnSync(executable, ["--version"], {
782
+ encoding: "utf8",
783
+ timeout: 5_000,
784
+ windowsHide: true,
785
+ env: chromeDevtoolsEnvironment(),
786
+ });
787
+ if (result.status !== 0) return undefined;
788
+ const raw = result.stdout.trim().split(/\s+/).pop() ?? "";
789
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(raw);
790
+ if (!match) return undefined;
791
+ return { raw, parts: match.slice(1, 4).map(Number) };
792
+ }
793
+
794
+ function compareVersions(left, right) {
795
+ for (let index = 0; index < 3; index += 1) {
796
+ if ((left[index] ?? 0) !== (right[index] ?? 0)) return (left[index] ?? 0) < (right[index] ?? 0) ? -1 : 1;
797
+ }
798
+ return 0;
799
+ }
800
+
801
+ function chromeDevtoolsEnvironment() {
802
+ const result = {};
803
+ for (const key of ["PATH", "HOME", "TMPDIR", "TEMP", "TMP", "SystemRoot", "WINDIR", "USERPROFILE", "DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR"]) {
804
+ if (process.env[key] !== undefined) result[key] = process.env[key];
805
+ }
806
+ return {
807
+ ...result,
808
+ CI: "1",
809
+ CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: "1",
810
+ CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS: "1",
811
+ };
812
+ }
813
+
814
+ async function retryUntil(check, timeoutMs, reason) {
815
+ const deadline = Date.now() + timeoutMs;
816
+ let lastError;
817
+ while (Date.now() <= deadline) {
818
+ try {
819
+ if (await check()) return;
820
+ } catch (error) {
821
+ lastError = error;
822
+ }
823
+ if (Date.now() >= deadline) break;
824
+ await sleep(Math.min(POLL_MS, deadline - Date.now()));
825
+ }
826
+ throw new Error(lastError ? `${reason}: ${safeReason(lastError)}` : reason);
827
+ }
828
+
829
+ function boundedActionTimeout(value, context) {
830
+ const requested = value === undefined ? DEFAULT_ACTION_TIMEOUT_MS : value;
831
+ if (!Number.isInteger(requested) || requested < 100 || requested > MAX_ACTION_TIMEOUT_MS) throw new Error(`timeoutMs must be between 100 and ${MAX_ACTION_TIMEOUT_MS}`);
832
+ return Math.max(100, Math.min(requested, boundedRemaining(context, requested)));
833
+ }
834
+
835
+ function boundedRemaining(context, maximum) {
836
+ return Math.max(100, Math.min(maximum, context.deadline - Date.now()));
837
+ }
838
+
839
+ function safeCliFailure(execution) {
840
+ if (execution.timedOut) return "command timed out";
841
+ return Number.isInteger(execution.code) ? `command exited with code ${execution.code}` : "command failed";
842
+ }
843
+
844
+ function evidenceName(value, index, fallback) {
845
+ const name = value === undefined ? `${fallback}-${index + 1}` : value;
846
+ if (typeof name !== "string" || !SAFE_NAME.test(name) || name === "." || name.includes("..")) throw new Error("evidence name must contain only letters, digits, dot, underscore, or dash without dot segments");
847
+ return name;
848
+ }
849
+
850
+ function numericRecord(value) {
851
+ const result = {};
852
+ for (const [key, entry] of Object.entries(value)) if (typeof entry === "number" && Number.isFinite(entry)) result[key] = entry;
853
+ return result;
854
+ }
855
+
856
+ function safeHttpUrlString(value) {
857
+ if (typeof value !== "string") return "";
858
+ try {
859
+ const url = new URL(value);
860
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password ? url.href : "";
861
+ } catch { return ""; }
862
+ }
863
+
864
+ function finiteNumberOrNull(value) {
865
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
866
+ }
867
+
868
+ function integerOrZero(value) {
869
+ return Number.isInteger(value) && value >= 0 ? value : 0;
870
+ }
871
+
872
+ function blocked(reason, remediation) {
873
+ return { status: "BLOCKED", reason, remediation, assertions: [], observations: [], artifacts: emptyArtifacts() };
874
+ }
875
+
876
+ function emptyArtifacts() {
877
+ return { screenshots: [], videos: [], traces: [], terminalCaptures: [], accessibilitySnapshots: [], observations: [], downloads: [] };
878
+ }
879
+
880
+ function assertKnownFields(value, allowed, label) {
881
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${label} contains unsupported field: ${key}`);
882
+ }
883
+
884
+ function sleep(ms) {
885
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
886
+ }
887
+
888
+ function safeReason(error) {
889
+ return error instanceof Error ? error.message : String(error);
890
+ }
891
+
892
+ function isObject(value) {
893
+ return value !== null && typeof value === "object" && !Array.isArray(value);
894
+ }
895
+