@wingman-ai/gateway 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +8 -0
  2. package/dist/agent/config/agentConfig.cjs +12 -0
  3. package/dist/agent/config/agentConfig.d.ts +22 -0
  4. package/dist/agent/config/agentConfig.js +10 -1
  5. package/dist/agent/config/agentLoader.cjs +9 -0
  6. package/dist/agent/config/agentLoader.js +9 -0
  7. package/dist/agent/config/toolRegistry.cjs +17 -0
  8. package/dist/agent/config/toolRegistry.d.ts +15 -0
  9. package/dist/agent/config/toolRegistry.js +17 -0
  10. package/dist/agent/tests/agentConfig.test.cjs +6 -1
  11. package/dist/agent/tests/agentConfig.test.js +6 -1
  12. package/dist/agent/tests/browserControlHelpers.test.cjs +35 -0
  13. package/dist/agent/tests/browserControlHelpers.test.d.ts +1 -0
  14. package/dist/agent/tests/browserControlHelpers.test.js +29 -0
  15. package/dist/agent/tests/browserControlTool.test.cjs +2117 -0
  16. package/dist/agent/tests/browserControlTool.test.d.ts +1 -0
  17. package/dist/agent/tests/browserControlTool.test.js +2111 -0
  18. package/dist/agent/tests/internet_search.test.cjs +22 -28
  19. package/dist/agent/tests/internet_search.test.js +22 -28
  20. package/dist/agent/tests/toolRegistry.test.cjs +6 -0
  21. package/dist/agent/tests/toolRegistry.test.js +6 -0
  22. package/dist/agent/tools/browser_control.cjs +1282 -0
  23. package/dist/agent/tools/browser_control.d.ts +478 -0
  24. package/dist/agent/tools/browser_control.js +1242 -0
  25. package/dist/agent/tools/internet_search.cjs +9 -5
  26. package/dist/agent/tools/internet_search.js +9 -5
  27. package/dist/cli/commands/agent.cjs +16 -2
  28. package/dist/cli/commands/agent.js +16 -2
  29. package/dist/cli/commands/browser.cjs +603 -0
  30. package/dist/cli/commands/browser.d.ts +13 -0
  31. package/dist/cli/commands/browser.js +566 -0
  32. package/dist/cli/commands/gateway.cjs +18 -7
  33. package/dist/cli/commands/gateway.d.ts +5 -1
  34. package/dist/cli/commands/gateway.js +18 -7
  35. package/dist/cli/commands/init.cjs +134 -45
  36. package/dist/cli/commands/init.js +134 -45
  37. package/dist/cli/commands/skill.cjs +3 -2
  38. package/dist/cli/commands/skill.js +3 -2
  39. package/dist/cli/config/loader.cjs +15 -0
  40. package/dist/cli/config/loader.js +15 -0
  41. package/dist/cli/config/schema.cjs +51 -2
  42. package/dist/cli/config/schema.d.ts +49 -0
  43. package/dist/cli/config/schema.js +44 -1
  44. package/dist/cli/core/workspace.cjs +89 -0
  45. package/dist/cli/core/workspace.d.ts +1 -0
  46. package/dist/cli/core/workspace.js +55 -0
  47. package/dist/cli/index.cjs +53 -5
  48. package/dist/cli/index.js +53 -5
  49. package/dist/cli/types/browser.cjs +18 -0
  50. package/dist/cli/types/browser.d.ts +9 -0
  51. package/dist/cli/types/browser.js +0 -0
  52. package/dist/gateway/browserRelayServer.cjs +338 -0
  53. package/dist/gateway/browserRelayServer.d.ts +38 -0
  54. package/dist/gateway/browserRelayServer.js +301 -0
  55. package/dist/gateway/http/agents.cjs +22 -0
  56. package/dist/gateway/http/agents.js +22 -0
  57. package/dist/gateway/http/fs.cjs +57 -0
  58. package/dist/gateway/http/fs.js +58 -1
  59. package/dist/gateway/server.cjs +43 -6
  60. package/dist/gateway/server.d.ts +4 -1
  61. package/dist/gateway/server.js +36 -5
  62. package/dist/gateway/transport/websocket.cjs +45 -10
  63. package/dist/gateway/transport/websocket.d.ts +1 -0
  64. package/dist/gateway/transport/websocket.js +41 -9
  65. package/dist/gateway/types.d.ts +4 -0
  66. package/dist/tests/agents-api.test.cjs +52 -0
  67. package/dist/tests/agents-api.test.js +53 -1
  68. package/dist/tests/browser-command.test.cjs +264 -0
  69. package/dist/tests/browser-command.test.d.ts +1 -0
  70. package/dist/tests/browser-command.test.js +258 -0
  71. package/dist/tests/browser-relay-server.test.cjs +20 -0
  72. package/dist/tests/browser-relay-server.test.d.ts +1 -0
  73. package/dist/tests/browser-relay-server.test.js +14 -0
  74. package/dist/tests/cli-config-loader.test.cjs +43 -0
  75. package/dist/tests/cli-config-loader.test.js +43 -0
  76. package/dist/tests/cli-init.test.cjs +25 -2
  77. package/dist/tests/cli-init.test.js +25 -2
  78. package/dist/tests/cli-workspace-root.test.cjs +114 -0
  79. package/dist/tests/cli-workspace-root.test.d.ts +1 -0
  80. package/dist/tests/cli-workspace-root.test.js +108 -0
  81. package/dist/tests/fs-api.test.cjs +138 -0
  82. package/dist/tests/fs-api.test.d.ts +1 -0
  83. package/dist/tests/fs-api.test.js +132 -0
  84. package/dist/tests/gateway-command-workspace.test.cjs +150 -0
  85. package/dist/tests/gateway-command-workspace.test.d.ts +1 -0
  86. package/dist/tests/gateway-command-workspace.test.js +144 -0
  87. package/dist/tests/gateway-request-execution-overrides.test.cjs +42 -0
  88. package/dist/tests/gateway-request-execution-overrides.test.d.ts +1 -0
  89. package/dist/tests/gateway-request-execution-overrides.test.js +36 -0
  90. package/dist/tests/gateway.test.cjs +31 -0
  91. package/dist/tests/gateway.test.js +31 -0
  92. package/dist/tests/websocket-transport.test.cjs +31 -0
  93. package/dist/tests/websocket-transport.test.d.ts +1 -0
  94. package/dist/tests/websocket-transport.test.js +25 -0
  95. package/dist/webui/assets/index-Cwkg4DKj.css +11 -0
  96. package/dist/webui/assets/{index-0nUBsUUq.js → index-DHbfLOUR.js} +109 -107
  97. package/dist/webui/index.html +2 -2
  98. package/extensions/wingman-browser-extension/README.md +27 -0
  99. package/extensions/wingman-browser-extension/background.js +416 -0
  100. package/extensions/wingman-browser-extension/manifest.json +19 -0
  101. package/extensions/wingman-browser-extension/options.html +156 -0
  102. package/extensions/wingman-browser-extension/options.js +106 -0
  103. package/package.json +8 -8
  104. package/{.wingman → templates}/agents/README.md +2 -1
  105. package/{.wingman → templates}/agents/coding/agent.md +0 -1
  106. package/{.wingman → templates}/agents/coding-v2/agent.md +0 -1
  107. package/{.wingman → templates}/agents/game-dev/agent.md +8 -1
  108. package/{.wingman → templates}/agents/game-dev/art-generation.md +1 -0
  109. package/{.wingman → templates}/agents/main/agent.md +5 -0
  110. package/{.wingman → templates}/agents/researcher/agent.md +9 -0
  111. package/{.wingman → templates}/agents/stock-trader/agent.md +1 -0
  112. package/dist/webui/assets/index-kk7OrD-G.css +0 -11
  113. /package/{.wingman → templates}/agents/coding-v2/implementor.md +0 -0
  114. /package/{.wingman → templates}/agents/game-dev/asset-refinement.md +0 -0
  115. /package/{.wingman → templates}/agents/game-dev/planning-idea.md +0 -0
  116. /package/{.wingman → templates}/agents/game-dev/ui-specialist.md +0 -0
  117. /package/{.wingman → templates}/agents/stock-trader/chain-curator.md +0 -0
  118. /package/{.wingman → templates}/agents/stock-trader/goal-translator.md +0 -0
  119. /package/{.wingman → templates}/agents/stock-trader/guardrails-veto.md +0 -0
  120. /package/{.wingman → templates}/agents/stock-trader/path-planner.md +0 -0
  121. /package/{.wingman → templates}/agents/stock-trader/regime-analyst.md +0 -0
  122. /package/{.wingman → templates}/agents/stock-trader/risk.md +0 -0
  123. /package/{.wingman → templates}/agents/stock-trader/selection.md +0 -0
  124. /package/{.wingman → templates}/agents/stock-trader/strategy-composer.md +0 -0
@@ -0,0 +1,1242 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { tool } from "langchain";
7
+ import { array, boolean as external_zod_boolean, discriminatedUnion, enum as external_zod_enum, literal, number, object, string } from "zod";
8
+ import { createLogger } from "../../logger.js";
9
+ const logger = createLogger();
10
+ const DEVTOOLS_ENDPOINT_REGEX = /DevTools listening on (ws:\/\/\S+)/i;
11
+ const DEFAULT_ACTION_TIMEOUT_MS = 30000;
12
+ const MAX_ACTION_TIMEOUT_MS = 300000;
13
+ const DEFAULT_LAUNCH_TIMEOUT_MS = 15000;
14
+ const DEFAULT_RELAY_CONNECT_TIMEOUT_MS = 5000;
15
+ const DEFAULT_RELAY_HOST = "127.0.0.1";
16
+ const DEFAULT_RELAY_PORT = 18792;
17
+ const DEFAULT_MAX_EXTRACT_CHARS = 5000;
18
+ const MAX_EXTRACT_CHARS = 1000000;
19
+ const MAX_ACTIONS = 25;
20
+ const DEFAULT_PROFILES_ROOT = ".wingman/browser-profiles";
21
+ const DEFAULT_EXTENSIONS_ROOT = ".wingman/browser-extensions";
22
+ const DEFAULT_BUNDLED_EXTENSION_ID = "wingman";
23
+ const BUNDLED_EXTENSION_RELATIVE_PATH = "../../../extensions/wingman-browser-extension";
24
+ const PERSISTENT_PROFILE_IGNORE_DEFAULT_ARGS = [
25
+ "--password-store=basic",
26
+ "--use-mock-keychain"
27
+ ];
28
+ const PROFILE_LOCK_FILENAME = ".wingman-browser.lock";
29
+ const PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
30
+ const CHROME_COMMON_ARGS = [
31
+ "--disable-extensions",
32
+ "--disable-background-networking",
33
+ "--disable-default-apps",
34
+ "--no-default-browser-check",
35
+ "--no-first-run",
36
+ "--disable-sync",
37
+ "--disable-component-update",
38
+ "--mute-audio",
39
+ "--hide-scrollbars"
40
+ ];
41
+ const NavigateActionSchema = object({
42
+ type: literal("navigate"),
43
+ url: string().url().describe("Destination URL")
44
+ });
45
+ const NavigateAliasActionSchema = object({
46
+ type: literal("url"),
47
+ url: string().url().describe("Destination URL")
48
+ });
49
+ const NavigateOpenAliasActionSchema = object({
50
+ type: literal("open"),
51
+ url: string().url().describe("Destination URL")
52
+ });
53
+ const NavigateGotoAliasActionSchema = object({
54
+ type: literal("goto"),
55
+ url: string().url().describe("Destination URL")
56
+ });
57
+ const ClickActionSchema = object({
58
+ type: literal("click"),
59
+ selector: string().min(1).describe("CSS selector to click")
60
+ });
61
+ const TypeActionSchema = object({
62
+ type: literal("type"),
63
+ selector: string().min(1).describe("CSS selector to target"),
64
+ text: string().describe("Text value to enter"),
65
+ submit: external_zod_boolean().optional().default(false).describe("Press Enter after typing")
66
+ });
67
+ const PressActionSchema = object({
68
+ type: literal("press"),
69
+ key: string().min(1).describe("Keyboard key (for example Enter, Tab, ArrowDown)")
70
+ });
71
+ const WaitActionSchema = object({
72
+ type: literal("wait"),
73
+ ms: number().int().min(1).max(MAX_ACTION_TIMEOUT_MS).optional().describe("How long to wait in milliseconds"),
74
+ selector: string().min(1).optional().describe("Wait for this selector to become visible"),
75
+ url: string().min(1).optional().describe("Wait for this URL/glob pattern"),
76
+ load: external_zod_enum([
77
+ "load",
78
+ "domcontentloaded",
79
+ "networkidle"
80
+ ]).optional().describe("Wait for this load state"),
81
+ fn: string().min(1).optional().describe("Wait for this JavaScript predicate to become truthy"),
82
+ timeoutMs: number().int().min(1).max(MAX_ACTION_TIMEOUT_MS).optional().describe("Optional timeout override in milliseconds")
83
+ }).refine((action)=>Boolean(action.ms || action.selector || action.url || action.load || action.fn), {
84
+ message: "wait requires ms or one of selector/url/load/fn"
85
+ });
86
+ const WaitAliasActionSchema = object({
87
+ type: literal("ms"),
88
+ ms: number().int().min(1).max(MAX_ACTION_TIMEOUT_MS).describe("How long to wait in milliseconds")
89
+ });
90
+ const WaitSleepAliasActionSchema = object({
91
+ type: literal("sleep"),
92
+ ms: number().int().min(1).max(MAX_ACTION_TIMEOUT_MS).describe("How long to wait in milliseconds")
93
+ });
94
+ const WaitPauseAliasActionSchema = object({
95
+ type: literal("pause"),
96
+ ms: number().int().min(1).max(MAX_ACTION_TIMEOUT_MS).describe("How long to wait in milliseconds")
97
+ });
98
+ const WaitForActionBaseSchema = object({
99
+ selector: string().min(1).optional().describe("Wait for this selector to become visible"),
100
+ url: string().min(1).optional().describe("Wait for this URL/glob pattern"),
101
+ load: external_zod_enum([
102
+ "load",
103
+ "domcontentloaded",
104
+ "networkidle"
105
+ ]).optional().describe("Wait for this load state"),
106
+ fn: string().min(1).optional().describe("Wait for this JavaScript predicate to become truthy"),
107
+ timeoutMs: number().int().min(1).max(MAX_ACTION_TIMEOUT_MS).optional().describe("Optional timeout override in milliseconds")
108
+ });
109
+ const WaitForActionSchema = WaitForActionBaseSchema.extend({
110
+ type: literal("wait_for")
111
+ }).refine((action)=>Boolean(action.selector || action.url || action.load || action.fn), {
112
+ message: "wait_for requires at least one of selector/url/load/fn"
113
+ });
114
+ const WaitUntilAliasActionSchema = WaitForActionBaseSchema.extend({
115
+ type: literal("wait_until")
116
+ }).refine((action)=>Boolean(action.selector || action.url || action.load || action.fn), {
117
+ message: "wait_until requires at least one of selector/url/load/fn"
118
+ });
119
+ const ExtractTextActionSchema = object({
120
+ type: literal("extract_text"),
121
+ selector: string().min(1).optional().describe("Optional CSS selector; defaults to body"),
122
+ maxChars: number().int().min(1).max(MAX_EXTRACT_CHARS).optional().default(DEFAULT_MAX_EXTRACT_CHARS).describe("Maximum returned characters")
123
+ });
124
+ const ExtractTextAliasActionSchema = object({
125
+ type: literal("selector"),
126
+ selector: string().min(1).describe("CSS selector"),
127
+ maxChars: number().int().min(1).max(MAX_EXTRACT_CHARS).optional().default(DEFAULT_MAX_EXTRACT_CHARS).describe("Maximum returned characters")
128
+ });
129
+ const ExtractAliasActionSchema = object({
130
+ type: literal("extract"),
131
+ selector: string().min(1).optional().describe("Optional CSS selector; defaults to body"),
132
+ maxChars: number().int().min(1).max(MAX_EXTRACT_CHARS).optional().default(DEFAULT_MAX_EXTRACT_CHARS).describe("Maximum returned characters")
133
+ });
134
+ const GetContentAliasActionSchema = object({
135
+ type: literal("getContent"),
136
+ selector: string().min(1).optional().describe("Optional CSS selector; defaults to body"),
137
+ maxChars: number().int().min(1).max(MAX_EXTRACT_CHARS).optional().default(DEFAULT_MAX_EXTRACT_CHARS).describe("Maximum returned characters")
138
+ });
139
+ const GetContentSnakeAliasActionSchema = object({
140
+ type: literal("get_content"),
141
+ selector: string().min(1).optional().describe("Optional CSS selector; defaults to body"),
142
+ maxChars: number().int().min(1).max(MAX_EXTRACT_CHARS).optional().default(DEFAULT_MAX_EXTRACT_CHARS).describe("Maximum returned characters")
143
+ });
144
+ const QuerySelectorAliasActionSchema = object({
145
+ type: literal("querySelector"),
146
+ selector: string().min(1).optional().describe("Optional CSS selector; defaults to body"),
147
+ maxChars: number().int().min(1).max(MAX_EXTRACT_CHARS).optional().default(DEFAULT_MAX_EXTRACT_CHARS).describe("Maximum returned characters")
148
+ });
149
+ const QuerySelectorSnakeAliasActionSchema = object({
150
+ type: literal("query_selector"),
151
+ selector: string().min(1).optional().describe("Optional CSS selector; defaults to body"),
152
+ maxChars: number().int().min(1).max(MAX_EXTRACT_CHARS).optional().default(DEFAULT_MAX_EXTRACT_CHARS).describe("Maximum returned characters")
153
+ });
154
+ const ScreenshotActionSchema = object({
155
+ type: literal("screenshot"),
156
+ path: string().min(1).optional().describe("Relative output path within workspace"),
157
+ fullPage: external_zod_boolean().optional().default(true).describe("Capture the full page")
158
+ });
159
+ const ScreenshotAliasActionSchema = object({
160
+ type: literal("path"),
161
+ path: string().min(1).describe("Relative output path within workspace"),
162
+ fullPage: external_zod_boolean().optional().default(true).describe("Capture the full page")
163
+ });
164
+ const SnapshotAliasActionSchema = object({
165
+ type: literal("snapshot"),
166
+ path: string().min(1).describe("Relative output path within workspace"),
167
+ fullPage: external_zod_boolean().optional().default(true).describe("Capture the full page")
168
+ });
169
+ const CaptureAliasActionSchema = object({
170
+ type: literal("capture"),
171
+ path: string().min(1).describe("Relative output path within workspace"),
172
+ fullPage: external_zod_boolean().optional().default(true).describe("Capture the full page")
173
+ });
174
+ const EvaluateActionSchema = object({
175
+ type: literal("evaluate"),
176
+ expression: string().min(1).describe("JavaScript expression to evaluate in page context")
177
+ });
178
+ const EvaluateAliasActionSchema = object({
179
+ type: literal("expression"),
180
+ expression: string().min(1).describe("JavaScript expression to evaluate in page context")
181
+ });
182
+ const EvaluateJsAliasActionSchema = object({
183
+ type: literal("js"),
184
+ expression: string().min(1).describe("JavaScript expression to evaluate in page context")
185
+ });
186
+ const EvaluateScriptAliasActionSchema = object({
187
+ type: literal("script"),
188
+ expression: string().min(1).describe("JavaScript expression to evaluate in page context")
189
+ });
190
+ const BrowserActionSchema = discriminatedUnion("type", [
191
+ NavigateActionSchema,
192
+ NavigateAliasActionSchema,
193
+ NavigateOpenAliasActionSchema,
194
+ NavigateGotoAliasActionSchema,
195
+ ClickActionSchema,
196
+ TypeActionSchema,
197
+ PressActionSchema,
198
+ WaitActionSchema,
199
+ WaitAliasActionSchema,
200
+ WaitSleepAliasActionSchema,
201
+ WaitPauseAliasActionSchema,
202
+ WaitForActionSchema,
203
+ WaitUntilAliasActionSchema,
204
+ ExtractTextActionSchema,
205
+ ExtractTextAliasActionSchema,
206
+ ExtractAliasActionSchema,
207
+ GetContentAliasActionSchema,
208
+ GetContentSnakeAliasActionSchema,
209
+ QuerySelectorAliasActionSchema,
210
+ QuerySelectorSnakeAliasActionSchema,
211
+ ScreenshotActionSchema,
212
+ ScreenshotAliasActionSchema,
213
+ SnapshotAliasActionSchema,
214
+ CaptureAliasActionSchema,
215
+ EvaluateActionSchema,
216
+ EvaluateAliasActionSchema,
217
+ EvaluateJsAliasActionSchema,
218
+ EvaluateScriptAliasActionSchema
219
+ ]);
220
+ const BrowserControlInputSchema = object({
221
+ url: string().url().optional().describe("Optional initial URL to open"),
222
+ actions: array(BrowserActionSchema).max(MAX_ACTIONS).optional().default([]).describe("Ordered browser actions to execute"),
223
+ headless: external_zod_boolean().optional().describe("Launch browser in headless mode. Non-persistent runs default to headless; persistent browser profiles default to headed unless headless is explicitly requested."),
224
+ timeoutMs: number().int().min(1000).max(MAX_ACTION_TIMEOUT_MS).optional().default(DEFAULT_ACTION_TIMEOUT_MS).describe("Per-action timeout in milliseconds"),
225
+ executablePath: string().min(1).optional().describe("Optional path to Chrome/Chromium binary. Falls back to WINGMAN_CHROME_EXECUTABLE or common install paths.")
226
+ });
227
+ const DEFAULT_BROWSER_CONTROL_DEPENDENCIES = {
228
+ importPlaywright: async ()=>await import("playwright-core"),
229
+ startChrome: startChromeWithDevtools,
230
+ resolveRelayWsEndpoint: resolveRelayWsEndpoint,
231
+ mkTempDir: ()=>mkdtempSync(join(tmpdir(), "wingman-browser-")),
232
+ removeDir: (target)=>rmSync(target, {
233
+ recursive: true,
234
+ force: true
235
+ }),
236
+ now: ()=>Date.now()
237
+ };
238
+ function getChromeCandidates() {
239
+ const candidates = [
240
+ process.env.WINGMAN_CHROME_EXECUTABLE,
241
+ "google-chrome",
242
+ "chromium-browser",
243
+ "chromium"
244
+ ].filter((candidate)=>Boolean(candidate?.trim()));
245
+ if ("darwin" === process.platform) candidates.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium");
246
+ if ("linux" === process.platform) candidates.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium");
247
+ if ("win32" === process.platform) candidates.push("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
248
+ return candidates;
249
+ }
250
+ function resolveBinaryFromPath(binaryName) {
251
+ const locator = "win32" === process.platform ? "where" : "which";
252
+ const result = spawnSync(locator, [
253
+ binaryName
254
+ ], {
255
+ encoding: "utf-8"
256
+ });
257
+ if (0 !== result.status || !result.stdout) return null;
258
+ const firstLine = result.stdout.split(/\r?\n/).map((line)=>line.trim()).find(Boolean);
259
+ return firstLine || null;
260
+ }
261
+ function resolveChromeExecutablePath(explicitPath) {
262
+ const candidatePool = explicitPath?.trim() ? [
263
+ explicitPath.trim()
264
+ ] : getChromeCandidates();
265
+ for (const candidate of candidatePool)if (candidate) {
266
+ if (isAbsolute(candidate) && existsSync(candidate)) return candidate;
267
+ if (!isAbsolute(candidate)) {
268
+ const fromPath = resolveBinaryFromPath(candidate);
269
+ if (fromPath) return fromPath;
270
+ }
271
+ }
272
+ if (explicitPath?.trim()) throw new Error(`Chrome executable not found at "${explicitPath}". Provide a valid executable path.`);
273
+ throw new Error("No Chrome/Chromium executable found. Install Chrome/Chromium or set WINGMAN_CHROME_EXECUTABLE.");
274
+ }
275
+ function waitForDevtoolsEndpoint(chromeProcess, launchTimeoutMs, userDataDir) {
276
+ return new Promise((resolveEndpoint, rejectEndpoint)=>{
277
+ let settled = false;
278
+ let logs = "";
279
+ const intervalHandle = setInterval(()=>{
280
+ const endpointFromFile = readDevtoolsEndpointFromFile(userDataDir);
281
+ if (!endpointFromFile) return;
282
+ finish(()=>resolveEndpoint(endpointFromFile));
283
+ }, 100);
284
+ const finish = (callback)=>{
285
+ if (settled) return;
286
+ settled = true;
287
+ clearTimeout(timeoutHandle);
288
+ clearInterval(intervalHandle);
289
+ chromeProcess.stdout.removeListener("data", onData);
290
+ chromeProcess.stderr.removeListener("data", onData);
291
+ chromeProcess.removeListener("error", onError);
292
+ chromeProcess.removeListener("exit", onExit);
293
+ callback();
294
+ };
295
+ const onData = (chunk)=>{
296
+ const text = chunk.toString();
297
+ logs += text;
298
+ const match = text.match(DEVTOOLS_ENDPOINT_REGEX);
299
+ if (!match?.[1]) return;
300
+ finish(()=>resolveEndpoint(match[1]));
301
+ };
302
+ const onError = (error)=>{
303
+ finish(()=>rejectEndpoint(new Error(`Failed to launch Chrome for CDP connection: ${error.message}`)));
304
+ };
305
+ const onExit = (code)=>{
306
+ finish(()=>rejectEndpoint(new Error(`Chrome exited before exposing DevTools endpoint (code: ${code ?? "unknown"}). Output: ${logs.trim() || "none"}`)));
307
+ };
308
+ const timeoutHandle = setTimeout(()=>{
309
+ finish(()=>rejectEndpoint(new Error(`Timed out waiting for DevTools endpoint after ${launchTimeoutMs}ms.`)));
310
+ }, launchTimeoutMs);
311
+ chromeProcess.stdout.on("data", onData);
312
+ chromeProcess.stderr.on("data", onData);
313
+ chromeProcess.on("error", onError);
314
+ chromeProcess.on("exit", onExit);
315
+ });
316
+ }
317
+ function readDevtoolsEndpointFromFile(userDataDir) {
318
+ const activePortPath = join(userDataDir, "DevToolsActivePort");
319
+ if (!existsSync(activePortPath)) return null;
320
+ try {
321
+ const raw = readFileSync(activePortPath, "utf-8").trim();
322
+ if (!raw) return null;
323
+ const [portLine, browserPathLine] = raw.split(/\r?\n/);
324
+ const port = Number.parseInt(portLine?.trim() || "", 10);
325
+ if (!Number.isFinite(port) || port <= 0) return null;
326
+ const rawPath = browserPathLine?.trim();
327
+ if (!rawPath) return null;
328
+ const browserPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
329
+ return `ws://127.0.0.1:${port}${browserPath}`;
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ async function closeChromeProcess(chromeProcess) {
335
+ if (null !== chromeProcess.exitCode || chromeProcess.killed) return;
336
+ await new Promise((resolveClose)=>{
337
+ let resolved = false;
338
+ const finish = ()=>{
339
+ if (resolved) return;
340
+ resolved = true;
341
+ resolveClose();
342
+ };
343
+ const forceKillTimeout = setTimeout(()=>{
344
+ if (null === chromeProcess.exitCode && !chromeProcess.killed) chromeProcess.kill("SIGKILL");
345
+ }, 2000);
346
+ chromeProcess.once("exit", ()=>{
347
+ clearTimeout(forceKillTimeout);
348
+ finish();
349
+ });
350
+ try {
351
+ chromeProcess.kill("SIGTERM");
352
+ } catch {
353
+ clearTimeout(forceKillTimeout);
354
+ finish();
355
+ }
356
+ });
357
+ }
358
+ function clearStaleDevtoolsArtifacts(userDataDir) {
359
+ const activePortPath = join(userDataDir, "DevToolsActivePort");
360
+ try {
361
+ unlinkSync(activePortPath);
362
+ } catch {}
363
+ }
364
+ async function startChromeWithDevtools(input) {
365
+ const executablePath = resolveChromeExecutablePath(input.executablePath);
366
+ clearStaleDevtoolsArtifacts(input.userDataDir);
367
+ const args = [
368
+ "--remote-debugging-port=0",
369
+ `--user-data-dir=${input.userDataDir}`,
370
+ ...input.chromeArgs || CHROME_COMMON_ARGS
371
+ ];
372
+ if (input.headless) args.push("--headless=new");
373
+ args.push("about:blank");
374
+ const chromeProcess = spawn(executablePath, args, {
375
+ stdio: [
376
+ "ignore",
377
+ "pipe",
378
+ "pipe"
379
+ ]
380
+ });
381
+ let wsEndpoint = "";
382
+ try {
383
+ wsEndpoint = await waitForDevtoolsEndpoint(chromeProcess, input.launchTimeoutMs, input.userDataDir);
384
+ } catch (error) {
385
+ await closeChromeProcess(chromeProcess);
386
+ throw error;
387
+ }
388
+ return {
389
+ wsEndpoint,
390
+ close: async ()=>{
391
+ await closeChromeProcess(chromeProcess);
392
+ }
393
+ };
394
+ }
395
+ function resolveBrowserTransportPreference(value) {
396
+ if ("playwright" === value || "relay" === value) return value;
397
+ return "auto";
398
+ }
399
+ function resolveRelayConfig(options) {
400
+ if (!options.relayConfig?.enabled) return null;
401
+ const host = (options.relayConfig.host || DEFAULT_RELAY_HOST).trim();
402
+ const port = Number.isInteger(options.relayConfig.port) ? Number(options.relayConfig.port) : DEFAULT_RELAY_PORT;
403
+ const requireAuth = false !== options.relayConfig.requireAuth;
404
+ const authToken = options.relayConfig.authToken?.trim() || void 0;
405
+ if (!host) throw new Error("Browser relay host cannot be empty.");
406
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid browser relay port: ${String(options.relayConfig.port)}`);
407
+ if (requireAuth && !authToken) throw new Error('Browser relay requires authToken. Run "wingman browser extension pair" and configure the extension token.');
408
+ return {
409
+ host,
410
+ port,
411
+ requireAuth,
412
+ authToken
413
+ };
414
+ }
415
+ async function resolveRelayWsEndpoint(config, timeoutMs) {
416
+ const relayHttpBase = `http://${config.host}:${config.port}`;
417
+ const controller = new AbortController();
418
+ const timer = setTimeout(()=>controller.abort(), timeoutMs);
419
+ try {
420
+ const versionResponse = await fetch(`${relayHttpBase}/json/version`, {
421
+ method: "GET",
422
+ signal: controller.signal
423
+ });
424
+ if (!versionResponse.ok) throw new Error(`Browser relay endpoint returned HTTP ${versionResponse.status}`);
425
+ const payload = await versionResponse.json();
426
+ if ("string" == typeof payload?.webSocketDebuggerUrl && payload.webSocketDebuggerUrl.trim()) return payload.webSocketDebuggerUrl.trim();
427
+ } catch (error) {
428
+ const suffix = error instanceof Error ? `: ${error.message}` : "";
429
+ throw new Error(`Failed to resolve browser relay endpoint${suffix}`);
430
+ } finally{
431
+ clearTimeout(timer);
432
+ }
433
+ const tokenParam = config.requireAuth && config.authToken ? `?token=${encodeURIComponent(config.authToken)}` : "";
434
+ return `ws://${config.host}:${config.port}/cdp${tokenParam}`;
435
+ }
436
+ function preferPersistentLaunch(options, _isPersistentProfile) {
437
+ if ("boolean" == typeof options.preferPersistentLaunch) return options.preferPersistentLaunch;
438
+ return _isPersistentProfile;
439
+ }
440
+ function resolveHeadlessMode(inputHeadless, isPersistentProfile) {
441
+ if ("boolean" == typeof inputHeadless) return inputHeadless;
442
+ return !isPersistentProfile;
443
+ }
444
+ function selectContext(contexts) {
445
+ if (0 === contexts.length) throw new Error("Failed to initialize browser context.");
446
+ const reversed = [
447
+ ...contexts
448
+ ].reverse();
449
+ return reversed.find((candidate)=>candidate.pages().length > 0) || reversed[0];
450
+ }
451
+ async function launchPersistentContext(playwright, userDataDir, executablePath, headless, startupTimeoutMs, chromeArgs) {
452
+ if ("function" != typeof playwright.chromium.launchPersistentContext) throw new Error("playwright-core runtime does not support launchPersistentContext.");
453
+ return playwright.chromium.launchPersistentContext(userDataDir, {
454
+ executablePath: resolveChromeExecutablePath(executablePath),
455
+ headless,
456
+ timeout: startupTimeoutMs,
457
+ args: chromeArgs,
458
+ ignoreDefaultArgs: PERSISTENT_PROFILE_IGNORE_DEFAULT_ARGS
459
+ });
460
+ }
461
+ function validateExtensionId(extensionId) {
462
+ const normalized = extensionId.trim();
463
+ if (!normalized) throw new Error("Extension ID cannot be empty.");
464
+ if (!PROFILE_ID_PATTERN.test(normalized)) throw new Error(`Invalid extension ID "${extensionId}". Use letters, numbers, dot, underscore, or dash.`);
465
+ return normalized;
466
+ }
467
+ function resolveExtensionPath(workspace, extensionId, options) {
468
+ const mappedPath = options.extensionPaths?.[extensionId];
469
+ if (mappedPath?.trim()) {
470
+ const trimmed = mappedPath.trim();
471
+ return isAbsolute(trimmed) ? trimmed : resolve(workspace, trimmed);
472
+ }
473
+ const rootDir = options.extensionsRootDir?.trim() || DEFAULT_EXTENSIONS_ROOT;
474
+ const absoluteRoot = isAbsolute(rootDir) ? rootDir : resolve(workspace, rootDir);
475
+ return join(absoluteRoot, extensionId);
476
+ }
477
+ function resolveBundledExtensionSourcePath() {
478
+ const bundledPath = resolve(fileURLToPath(new URL(BUNDLED_EXTENSION_RELATIVE_PATH, import.meta.url)));
479
+ const bundledManifest = join(bundledPath, "manifest.json");
480
+ return existsSync(bundledManifest) ? bundledPath : null;
481
+ }
482
+ function ensureBundledWingmanExtension(extensionPath) {
483
+ const bundledSourcePath = resolveBundledExtensionSourcePath();
484
+ if (!bundledSourcePath) return false;
485
+ mkdirSync(dirname(extensionPath), {
486
+ recursive: true
487
+ });
488
+ cpSync(bundledSourcePath, extensionPath, {
489
+ recursive: true,
490
+ force: true
491
+ });
492
+ return true;
493
+ }
494
+ function resolveEnabledExtensions(workspace, options) {
495
+ const requestedIds = options.browserExtensions?.length ? options.browserExtensions : options.defaultExtensions;
496
+ if (!requestedIds?.length) return {
497
+ extensionIds: [],
498
+ extensionDirs: []
499
+ };
500
+ const uniqueIds = Array.from(new Set(requestedIds.map((value)=>validateExtensionId(value))));
501
+ const extensionDirs = uniqueIds.map((extensionId)=>{
502
+ const extensionPath = resolveExtensionPath(workspace, extensionId, options);
503
+ if (!existsSync(extensionPath)) {
504
+ if (extensionId === DEFAULT_BUNDLED_EXTENSION_ID && ensureBundledWingmanExtension(extensionPath)) logger.info(`browser_control auto-provisioned bundled extension "${extensionId}" at ${extensionPath}`);
505
+ }
506
+ if (!existsSync(extensionPath)) throw new Error(`Configured extension path does not exist for "${extensionId}": ${extensionPath}`);
507
+ const manifestPath = join(extensionPath, "manifest.json");
508
+ if (!existsSync(manifestPath)) throw new Error(`manifest.json not found for extension "${extensionId}" at ${extensionPath}`);
509
+ return extensionPath;
510
+ });
511
+ return {
512
+ extensionIds: uniqueIds,
513
+ extensionDirs
514
+ };
515
+ }
516
+ function buildChromeArgs(extensionDirs) {
517
+ if (0 === extensionDirs.length) return CHROME_COMMON_ARGS;
518
+ const joined = extensionDirs.join(",");
519
+ return [
520
+ ...CHROME_COMMON_ARGS.filter((arg)=>"--disable-extensions" !== arg),
521
+ `--disable-extensions-except=${joined}`,
522
+ `--load-extension=${joined}`
523
+ ];
524
+ }
525
+ function validateProfileId(profileId) {
526
+ const normalized = profileId.trim();
527
+ if (!normalized) throw new Error("browserProfile cannot be empty.");
528
+ if (!PROFILE_ID_PATTERN.test(normalized)) throw new Error(`Invalid browserProfile "${profileId}". Use letters, numbers, dot, underscore, or dash.`);
529
+ return normalized;
530
+ }
531
+ function resolveProfilePath(workspace, profileId, options) {
532
+ const normalizedProfileId = validateProfileId(profileId);
533
+ const mappedPath = options.profilePaths?.[normalizedProfileId];
534
+ if (mappedPath && mappedPath.trim()) {
535
+ const trimmed = mappedPath.trim();
536
+ return isAbsolute(trimmed) ? trimmed : resolve(workspace, trimmed);
537
+ }
538
+ const rootDir = options.profilesRootDir?.trim() || DEFAULT_PROFILES_ROOT;
539
+ const absoluteRoot = isAbsolute(rootDir) ? rootDir : resolve(workspace, rootDir);
540
+ return join(absoluteRoot, normalizedProfileId);
541
+ }
542
+ function readProfileLockMetadata(lockPath) {
543
+ try {
544
+ const raw = readFileSync(lockPath, "utf-8");
545
+ const parsed = JSON.parse(raw);
546
+ if (!parsed || "object" != typeof parsed) return null;
547
+ return parsed;
548
+ } catch {
549
+ return null;
550
+ }
551
+ }
552
+ function isPidAlive(pid) {
553
+ if (!Number.isInteger(pid) || pid <= 0) return false;
554
+ try {
555
+ process.kill(pid, 0);
556
+ return true;
557
+ } catch (error) {
558
+ if (error && "object" == typeof error && "code" in error && "EPERM" === error.code) return true;
559
+ return false;
560
+ }
561
+ }
562
+ function acquireProfileLock(profileDir) {
563
+ const lockPath = join(profileDir, PROFILE_LOCK_FILENAME);
564
+ const writeLock = ()=>writeFileSync(lockPath, JSON.stringify({
565
+ pid: process.pid,
566
+ createdAt: new Date().toISOString()
567
+ }), {
568
+ flag: "wx"
569
+ });
570
+ try {
571
+ writeLock();
572
+ } catch (error) {
573
+ if (!(error instanceof Error && "code" in error && "EEXIST" === error.code)) throw error;
574
+ const lockMetadata = readProfileLockMetadata(lockPath);
575
+ if ("number" == typeof lockMetadata?.pid && lockMetadata.pid === process.pid) return ()=>{};
576
+ const stalePid = "number" == typeof lockMetadata?.pid && !isPidAlive(lockMetadata.pid);
577
+ if (!stalePid) throw new Error(`Browser profile is already in use: ${profileDir}. Wait for the other run to finish.`);
578
+ try {
579
+ unlinkSync(lockPath);
580
+ } catch {
581
+ throw new Error(`Browser profile is already in use: ${profileDir}. Wait for the other run to finish.`);
582
+ }
583
+ try {
584
+ writeLock();
585
+ } catch {
586
+ throw new Error(`Browser profile is already in use: ${profileDir}. Wait for the other run to finish.`);
587
+ }
588
+ }
589
+ let released = false;
590
+ return ()=>{
591
+ if (released) return;
592
+ released = true;
593
+ try {
594
+ unlinkSync(lockPath);
595
+ } catch {}
596
+ };
597
+ }
598
+ function resolveUserDataDir(workspace, options, dependencies) {
599
+ const configuredProfile = options.browserProfile?.trim();
600
+ if (!configuredProfile) return {
601
+ userDataDir: dependencies.mkTempDir(),
602
+ persistentProfile: false
603
+ };
604
+ const profileId = validateProfileId(configuredProfile);
605
+ const profileDir = resolveProfilePath(workspace, profileId, options);
606
+ mkdirSync(profileDir, {
607
+ recursive: true
608
+ });
609
+ const releaseLock = acquireProfileLock(profileDir);
610
+ return {
611
+ userDataDir: profileDir,
612
+ persistentProfile: true,
613
+ profileId,
614
+ releaseLock
615
+ };
616
+ }
617
+ function resolveWorkspaceRelativePath(workspace, targetPath) {
618
+ const absolutePath = resolve(workspace, targetPath);
619
+ const relativePath = relative(resolve(workspace), absolutePath);
620
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error("Output path must stay inside the workspace.");
621
+ return absolutePath;
622
+ }
623
+ function resolveScreenshotPath(workspace, requestedPath, now, actionIndex) {
624
+ if (requestedPath && isAbsolute(requestedPath)) throw new Error("Screenshot path must be relative to the workspace.");
625
+ const fallback = join(".wingman", "browser", `screenshot-${now()}-${actionIndex + 1}.png`);
626
+ const relativeOutputPath = requestedPath || fallback;
627
+ const absoluteOutputPath = resolveWorkspaceRelativePath(workspace, relativeOutputPath);
628
+ mkdirSync(dirname(absoluteOutputPath), {
629
+ recursive: true
630
+ });
631
+ return {
632
+ absolute: absoluteOutputPath,
633
+ relative: relative(workspace, absoluteOutputPath).split("\\").join("/")
634
+ };
635
+ }
636
+ function serializeEvaluation(value) {
637
+ if ("string" == typeof value || "number" == typeof value || "boolean" == typeof value || null === value) return value;
638
+ try {
639
+ return JSON.parse(JSON.stringify(value));
640
+ } catch {
641
+ return String(value);
642
+ }
643
+ }
644
+ function globToRegex(globPattern) {
645
+ let regex = "^";
646
+ for(let index = 0; index < globPattern.length; index += 1){
647
+ const current = globPattern[index];
648
+ const next = globPattern[index + 1];
649
+ if ("*" === current && "*" === next) {
650
+ regex += ".*";
651
+ index += 1;
652
+ continue;
653
+ }
654
+ if ("*" === current) {
655
+ regex += "[^/]*";
656
+ continue;
657
+ }
658
+ regex += current.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&");
659
+ }
660
+ regex += "$";
661
+ return new RegExp(regex);
662
+ }
663
+ async function waitForUrlFallback(page, urlPattern, timeoutMs) {
664
+ const regex = globToRegex(urlPattern);
665
+ const startedAt = Date.now();
666
+ while(Date.now() - startedAt < timeoutMs){
667
+ if (regex.test(page.url())) return;
668
+ await page.waitForTimeout(100);
669
+ }
670
+ throw new Error(`Timed out waiting for URL pattern "${urlPattern}".`);
671
+ }
672
+ async function waitForPredicateFallback(page, expression, timeoutMs) {
673
+ const startedAt = Date.now();
674
+ while(Date.now() - startedAt < timeoutMs){
675
+ const result = await page.evaluate(expression);
676
+ if (result) return;
677
+ await page.waitForTimeout(100);
678
+ }
679
+ throw new Error("Timed out waiting for JavaScript predicate to become truthy.");
680
+ }
681
+ async function runConditionalWait(page, action, timeoutMs) {
682
+ const waitTimeoutMs = action.timeoutMs ?? timeoutMs;
683
+ if (action.selector) if ("function" == typeof page.waitForSelector) await page.waitForSelector(action.selector, {
684
+ state: "visible",
685
+ timeout: waitTimeoutMs
686
+ });
687
+ else await page.textContent(action.selector, {
688
+ timeout: waitTimeoutMs
689
+ });
690
+ if (action.url) if ("function" == typeof page.waitForURL) await page.waitForURL(globToRegex(action.url), {
691
+ timeout: waitTimeoutMs
692
+ });
693
+ else await waitForUrlFallback(page, action.url, waitTimeoutMs);
694
+ if (action.load && "function" == typeof page.waitForLoadState) await page.waitForLoadState(action.load, {
695
+ timeout: waitTimeoutMs
696
+ });
697
+ if (action.fn) if ("function" == typeof page.waitForFunction) await page.waitForFunction(action.fn, void 0, {
698
+ timeout: waitTimeoutMs
699
+ });
700
+ else await waitForPredicateFallback(page, action.fn, waitTimeoutMs);
701
+ return {
702
+ type: "wait_for",
703
+ selector: action.selector || null,
704
+ url: action.url || null,
705
+ load: action.load || null,
706
+ fn: action.fn || null,
707
+ timeoutMs: waitTimeoutMs
708
+ };
709
+ }
710
+ async function runAction(page, action, timeoutMs, workspace, now, actionIndex) {
711
+ switch(action.type){
712
+ case "navigate":
713
+ await page.goto(action.url, {
714
+ waitUntil: "domcontentloaded",
715
+ timeout: timeoutMs
716
+ });
717
+ return {
718
+ type: action.type,
719
+ url: action.url
720
+ };
721
+ case "url":
722
+ await page.goto(action.url, {
723
+ waitUntil: "domcontentloaded",
724
+ timeout: timeoutMs
725
+ });
726
+ return {
727
+ type: "navigate",
728
+ url: action.url
729
+ };
730
+ case "open":
731
+ await page.goto(action.url, {
732
+ waitUntil: "domcontentloaded",
733
+ timeout: timeoutMs
734
+ });
735
+ return {
736
+ type: "navigate",
737
+ url: action.url
738
+ };
739
+ case "goto":
740
+ await page.goto(action.url, {
741
+ waitUntil: "domcontentloaded",
742
+ timeout: timeoutMs
743
+ });
744
+ return {
745
+ type: "navigate",
746
+ url: action.url
747
+ };
748
+ case "click":
749
+ await page.click(action.selector, {
750
+ timeout: timeoutMs
751
+ });
752
+ return {
753
+ type: action.type,
754
+ selector: action.selector
755
+ };
756
+ case "type":
757
+ await page.fill(action.selector, action.text, {
758
+ timeout: timeoutMs
759
+ });
760
+ if (action.submit) await page.keyboard.press("Enter");
761
+ return {
762
+ type: action.type,
763
+ selector: action.selector,
764
+ textLength: action.text.length,
765
+ submit: Boolean(action.submit)
766
+ };
767
+ case "press":
768
+ await page.keyboard.press(action.key);
769
+ return {
770
+ type: action.type,
771
+ key: action.key
772
+ };
773
+ case "wait":
774
+ if ("number" == typeof action.ms) {
775
+ await page.waitForTimeout(action.ms);
776
+ return {
777
+ type: action.type,
778
+ ms: action.ms
779
+ };
780
+ }
781
+ return runConditionalWait(page, {
782
+ type: "wait_for",
783
+ selector: action.selector,
784
+ url: action.url,
785
+ load: action.load,
786
+ fn: action.fn,
787
+ timeoutMs: action.timeoutMs
788
+ }, timeoutMs);
789
+ case "ms":
790
+ await page.waitForTimeout(action.ms);
791
+ return {
792
+ type: "wait",
793
+ ms: action.ms
794
+ };
795
+ case "sleep":
796
+ await page.waitForTimeout(action.ms);
797
+ return {
798
+ type: "wait",
799
+ ms: action.ms
800
+ };
801
+ case "pause":
802
+ await page.waitForTimeout(action.ms);
803
+ return {
804
+ type: "wait",
805
+ ms: action.ms
806
+ };
807
+ case "wait_for":
808
+ return runConditionalWait(page, action, timeoutMs);
809
+ case "wait_until":
810
+ return runConditionalWait(page, action, timeoutMs);
811
+ case "extract_text":
812
+ {
813
+ const selector = action.selector || "body";
814
+ const content = await page.textContent(selector, {
815
+ timeout: timeoutMs
816
+ });
817
+ const text = content || "";
818
+ const maxChars = action.maxChars ?? DEFAULT_MAX_EXTRACT_CHARS;
819
+ return {
820
+ type: action.type,
821
+ selector,
822
+ text: text.slice(0, maxChars),
823
+ truncated: text.length > maxChars
824
+ };
825
+ }
826
+ case "selector":
827
+ {
828
+ const content = await page.textContent(action.selector, {
829
+ timeout: timeoutMs
830
+ });
831
+ const text = content || "";
832
+ const maxChars = action.maxChars ?? DEFAULT_MAX_EXTRACT_CHARS;
833
+ return {
834
+ type: "extract_text",
835
+ selector: action.selector,
836
+ text: text.slice(0, maxChars),
837
+ truncated: text.length > maxChars
838
+ };
839
+ }
840
+ case "extract":
841
+ {
842
+ const selector = action.selector || "body";
843
+ const content = await page.textContent(selector, {
844
+ timeout: timeoutMs
845
+ });
846
+ const text = content || "";
847
+ const maxChars = action.maxChars ?? DEFAULT_MAX_EXTRACT_CHARS;
848
+ return {
849
+ type: "extract_text",
850
+ selector,
851
+ text: text.slice(0, maxChars),
852
+ truncated: text.length > maxChars
853
+ };
854
+ }
855
+ case "getContent":
856
+ {
857
+ const selector = action.selector || "body";
858
+ const content = await page.textContent(selector, {
859
+ timeout: timeoutMs
860
+ });
861
+ const text = content || "";
862
+ const maxChars = action.maxChars ?? DEFAULT_MAX_EXTRACT_CHARS;
863
+ return {
864
+ type: "extract_text",
865
+ selector,
866
+ text: text.slice(0, maxChars),
867
+ truncated: text.length > maxChars
868
+ };
869
+ }
870
+ case "get_content":
871
+ {
872
+ const selector = action.selector || "body";
873
+ const content = await page.textContent(selector, {
874
+ timeout: timeoutMs
875
+ });
876
+ const text = content || "";
877
+ const maxChars = action.maxChars ?? DEFAULT_MAX_EXTRACT_CHARS;
878
+ return {
879
+ type: "extract_text",
880
+ selector,
881
+ text: text.slice(0, maxChars),
882
+ truncated: text.length > maxChars
883
+ };
884
+ }
885
+ case "querySelector":
886
+ {
887
+ const selector = action.selector || "body";
888
+ const content = await page.textContent(selector, {
889
+ timeout: timeoutMs
890
+ });
891
+ const text = content || "";
892
+ const maxChars = action.maxChars ?? DEFAULT_MAX_EXTRACT_CHARS;
893
+ return {
894
+ type: "extract_text",
895
+ selector,
896
+ text: text.slice(0, maxChars),
897
+ truncated: text.length > maxChars
898
+ };
899
+ }
900
+ case "query_selector":
901
+ {
902
+ const selector = action.selector || "body";
903
+ const content = await page.textContent(selector, {
904
+ timeout: timeoutMs
905
+ });
906
+ const text = content || "";
907
+ const maxChars = action.maxChars ?? DEFAULT_MAX_EXTRACT_CHARS;
908
+ return {
909
+ type: "extract_text",
910
+ selector,
911
+ text: text.slice(0, maxChars),
912
+ truncated: text.length > maxChars
913
+ };
914
+ }
915
+ case "screenshot":
916
+ {
917
+ const screenshotPath = resolveScreenshotPath(workspace, action.path, now, actionIndex);
918
+ await page.screenshot({
919
+ path: screenshotPath.absolute,
920
+ fullPage: action.fullPage
921
+ });
922
+ return {
923
+ type: action.type,
924
+ path: screenshotPath.relative,
925
+ fullPage: Boolean(action.fullPage)
926
+ };
927
+ }
928
+ case "path":
929
+ {
930
+ const screenshotPath = resolveScreenshotPath(workspace, action.path, now, actionIndex);
931
+ await page.screenshot({
932
+ path: screenshotPath.absolute,
933
+ fullPage: action.fullPage
934
+ });
935
+ return {
936
+ type: "screenshot",
937
+ path: screenshotPath.relative,
938
+ fullPage: Boolean(action.fullPage)
939
+ };
940
+ }
941
+ case "snapshot":
942
+ {
943
+ const screenshotPath = resolveScreenshotPath(workspace, action.path, now, actionIndex);
944
+ await page.screenshot({
945
+ path: screenshotPath.absolute,
946
+ fullPage: action.fullPage
947
+ });
948
+ return {
949
+ type: "screenshot",
950
+ path: screenshotPath.relative,
951
+ fullPage: Boolean(action.fullPage)
952
+ };
953
+ }
954
+ case "capture":
955
+ {
956
+ const screenshotPath = resolveScreenshotPath(workspace, action.path, now, actionIndex);
957
+ await page.screenshot({
958
+ path: screenshotPath.absolute,
959
+ fullPage: action.fullPage
960
+ });
961
+ return {
962
+ type: "screenshot",
963
+ path: screenshotPath.relative,
964
+ fullPage: Boolean(action.fullPage)
965
+ };
966
+ }
967
+ case "evaluate":
968
+ {
969
+ const result = await page.evaluate(action.expression);
970
+ return {
971
+ type: action.type,
972
+ expression: action.expression,
973
+ result: serializeEvaluation(result)
974
+ };
975
+ }
976
+ case "expression":
977
+ {
978
+ const result = await page.evaluate(action.expression);
979
+ return {
980
+ type: "evaluate",
981
+ expression: action.expression,
982
+ result: serializeEvaluation(result)
983
+ };
984
+ }
985
+ case "js":
986
+ {
987
+ const result = await page.evaluate(action.expression);
988
+ return {
989
+ type: "evaluate",
990
+ expression: action.expression,
991
+ result: serializeEvaluation(result)
992
+ };
993
+ }
994
+ case "script":
995
+ {
996
+ const result = await page.evaluate(action.expression);
997
+ return {
998
+ type: "evaluate",
999
+ expression: action.expression,
1000
+ result: serializeEvaluation(result)
1001
+ };
1002
+ }
1003
+ default:
1004
+ throw new Error(`Unsupported browser action type: ${action.type}`);
1005
+ }
1006
+ }
1007
+ const createBrowserControlTool = (options = {}, dependencies = {})=>{
1008
+ const workspace = options.workspace || process.cwd();
1009
+ const configWorkspace = options.configWorkspace || workspace;
1010
+ const launchTimeoutMs = options.launchTimeoutMs || DEFAULT_LAUNCH_TIMEOUT_MS;
1011
+ const deps = {
1012
+ ...DEFAULT_BROWSER_CONTROL_DEPENDENCIES,
1013
+ ...dependencies
1014
+ };
1015
+ return tool(async (input)=>{
1016
+ let userDataDirSelection = null;
1017
+ let chromeSession = null;
1018
+ let browser = null;
1019
+ let launchedContext = null;
1020
+ let browserTransport = "cdp";
1021
+ let transportFallbackReason = null;
1022
+ const transportRequested = resolveBrowserTransportPreference(options.browserTransport);
1023
+ let reusedExistingCdpSession = false;
1024
+ let context = null;
1025
+ try {
1026
+ userDataDirSelection = resolveUserDataDir(configWorkspace, options, deps);
1027
+ const selectedUserDataDir = userDataDirSelection;
1028
+ if (!selectedUserDataDir) throw new Error("Failed to resolve browser user data directory.");
1029
+ const headless = resolveHeadlessMode(input.headless, selectedUserDataDir.persistentProfile);
1030
+ const timeoutMs = input.timeoutMs ?? DEFAULT_ACTION_TIMEOUT_MS;
1031
+ const startupTimeoutMs = Math.max(launchTimeoutMs, timeoutMs);
1032
+ const playwright = await deps.importPlaywright();
1033
+ const relayConfig = resolveRelayConfig(options);
1034
+ const executablePath = input.executablePath || options.defaultExecutablePath;
1035
+ const { extensionIds, extensionDirs } = resolveEnabledExtensions(configWorkspace, options);
1036
+ const chromeArgs = buildChromeArgs(extensionDirs);
1037
+ const usePersistentLaunch = preferPersistentLaunch(options, selectedUserDataDir.persistentProfile);
1038
+ const closeTransientSessions = async ()=>{
1039
+ if (launchedContext?.close) {
1040
+ try {
1041
+ await launchedContext.close();
1042
+ } catch {}
1043
+ launchedContext = null;
1044
+ }
1045
+ if (browser) {
1046
+ if (!reusedExistingCdpSession) try {
1047
+ await browser.close();
1048
+ } catch {}
1049
+ browser = null;
1050
+ }
1051
+ if (chromeSession) {
1052
+ try {
1053
+ await chromeSession.close();
1054
+ } catch {}
1055
+ chromeSession = null;
1056
+ }
1057
+ context = null;
1058
+ reusedExistingCdpSession = false;
1059
+ };
1060
+ const initializeViaRelay = async (connectTimeoutMs)=>{
1061
+ if (!relayConfig) throw new Error("Browser relay transport requested but browser.relay.enabled is false.");
1062
+ const relayWsEndpoint = await deps.resolveRelayWsEndpoint(relayConfig, connectTimeoutMs);
1063
+ browser = await playwright.chromium.connectOverCDP(relayWsEndpoint, {
1064
+ timeout: connectTimeoutMs
1065
+ });
1066
+ context = selectContext(browser.contexts());
1067
+ browserTransport = "relay-cdp";
1068
+ reusedExistingCdpSession = false;
1069
+ };
1070
+ const initializeViaPlaywright = async ()=>{
1071
+ if (usePersistentLaunch) {
1072
+ launchedContext = await launchPersistentContext(playwright, selectedUserDataDir.userDataDir, executablePath, headless, startupTimeoutMs, chromeArgs);
1073
+ context = launchedContext;
1074
+ browserTransport = "persistent-context";
1075
+ return;
1076
+ }
1077
+ if (selectedUserDataDir.persistentProfile) {
1078
+ const existingWsEndpoint = readDevtoolsEndpointFromFile(selectedUserDataDir.userDataDir);
1079
+ if (existingWsEndpoint) try {
1080
+ browser = await playwright.chromium.connectOverCDP(existingWsEndpoint, {
1081
+ timeout: startupTimeoutMs
1082
+ });
1083
+ context = selectContext(browser.contexts());
1084
+ reusedExistingCdpSession = true;
1085
+ } catch (reuseError) {
1086
+ logger.warn(`browser_control failed to attach to existing CDP endpoint, launching a fresh session: ${reuseError instanceof Error ? reuseError.message : String(reuseError)}`);
1087
+ if (browser) {
1088
+ try {
1089
+ await browser.close();
1090
+ } catch {}
1091
+ browser = null;
1092
+ }
1093
+ }
1094
+ }
1095
+ if (!context) {
1096
+ chromeSession = await deps.startChrome({
1097
+ executablePath,
1098
+ headless,
1099
+ launchTimeoutMs: startupTimeoutMs,
1100
+ userDataDir: selectedUserDataDir.userDataDir,
1101
+ chromeArgs
1102
+ });
1103
+ try {
1104
+ browser = await playwright.chromium.connectOverCDP(chromeSession.wsEndpoint, {
1105
+ timeout: startupTimeoutMs
1106
+ });
1107
+ context = selectContext(browser.contexts());
1108
+ } catch (cdpError) {
1109
+ const cdpMessage = cdpError instanceof Error ? cdpError.message : String(cdpError);
1110
+ if (browser) {
1111
+ try {
1112
+ await browser.close();
1113
+ } catch {}
1114
+ browser = null;
1115
+ }
1116
+ if (chromeSession) {
1117
+ try {
1118
+ await chromeSession.close();
1119
+ } catch {}
1120
+ chromeSession = null;
1121
+ }
1122
+ if (selectedUserDataDir.persistentProfile) {
1123
+ logger.warn(`browser_control CDP connection failed for persistent profile, retrying CDP once: ${cdpMessage}`);
1124
+ try {
1125
+ chromeSession = await deps.startChrome({
1126
+ executablePath,
1127
+ headless,
1128
+ launchTimeoutMs: startupTimeoutMs,
1129
+ userDataDir: selectedUserDataDir.userDataDir,
1130
+ chromeArgs
1131
+ });
1132
+ browser = await playwright.chromium.connectOverCDP(chromeSession.wsEndpoint, {
1133
+ timeout: startupTimeoutMs
1134
+ });
1135
+ context = selectContext(browser.contexts());
1136
+ } catch (retryError) {
1137
+ const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
1138
+ if (browser) {
1139
+ try {
1140
+ await browser.close();
1141
+ } catch {}
1142
+ browser = null;
1143
+ }
1144
+ if (chromeSession) {
1145
+ try {
1146
+ await chromeSession.close();
1147
+ } catch {}
1148
+ chromeSession = null;
1149
+ }
1150
+ const launchPersistent = playwright.chromium.launchPersistentContext;
1151
+ if ("function" != typeof launchPersistent) throw new Error(`CDP connection failed for persistent profile after retry. Initial error: ${cdpMessage}. Retry error: ${retryMessage}`);
1152
+ logger.warn(`browser_control CDP retry failed for persistent profile, falling back to persistent launch: ${retryMessage}`);
1153
+ launchedContext = await launchPersistentContext(playwright, selectedUserDataDir.userDataDir, executablePath, headless, startupTimeoutMs, chromeArgs);
1154
+ context = launchedContext;
1155
+ browserTransport = "persistent-context";
1156
+ }
1157
+ } else {
1158
+ const launchPersistent = playwright.chromium.launchPersistentContext;
1159
+ if ("function" != typeof launchPersistent) throw cdpError;
1160
+ logger.warn(`browser_control CDP connection failed, retrying with persistent launch: ${cdpMessage}`);
1161
+ launchedContext = await launchPersistentContext(playwright, selectedUserDataDir.userDataDir, executablePath, headless, startupTimeoutMs, chromeArgs);
1162
+ context = launchedContext;
1163
+ browserTransport = "persistent-context";
1164
+ }
1165
+ }
1166
+ }
1167
+ };
1168
+ if ("relay" === transportRequested) await initializeViaRelay(startupTimeoutMs);
1169
+ else try {
1170
+ await initializeViaPlaywright();
1171
+ } catch (playwrightError) {
1172
+ if ("auto" !== transportRequested || !relayConfig) throw playwrightError;
1173
+ await closeTransientSessions();
1174
+ const relayTimeoutMs = Math.min(startupTimeoutMs, DEFAULT_RELAY_CONNECT_TIMEOUT_MS);
1175
+ const playwrightMessage = playwrightError instanceof Error ? playwrightError.message : String(playwrightError);
1176
+ logger.warn(`browser_control playwright initialization failed in auto mode, falling back to relay: ${playwrightMessage}`);
1177
+ await initializeViaRelay(relayTimeoutMs);
1178
+ transportFallbackReason = `playwright initialization failed: ${playwrightMessage}`;
1179
+ }
1180
+ const resolvedContext = context;
1181
+ if (!resolvedContext) throw new Error("Failed to initialize browser context.");
1182
+ const activeContext = resolvedContext;
1183
+ let page = activeContext.pages().at(-1);
1184
+ if (!page) page = await activeContext.newPage();
1185
+ if ("function" == typeof page.bringToFront) await page.bringToFront();
1186
+ if (input.url) await page.goto(input.url, {
1187
+ waitUntil: "domcontentloaded",
1188
+ timeout: timeoutMs
1189
+ });
1190
+ const actionResults = [];
1191
+ for (const [index, action] of (input.actions || []).entries()){
1192
+ const result = await runAction(page, action, timeoutMs, workspace, deps.now, index);
1193
+ actionResults.push(result);
1194
+ }
1195
+ const summary = {
1196
+ browser: "cdp" === browserTransport ? "chrome-cdp" : "persistent-context" === browserTransport ? "chrome-playwright" : "chrome-relay",
1197
+ transport: browserTransport,
1198
+ transportRequested,
1199
+ transportUsed: browserTransport,
1200
+ fallbackReason: transportFallbackReason,
1201
+ mode: headless ? "headless" : "headed",
1202
+ persistentProfile: selectedUserDataDir.persistentProfile,
1203
+ profileId: selectedUserDataDir.profileId || null,
1204
+ profilePath: selectedUserDataDir.persistentProfile ? selectedUserDataDir.userDataDir : null,
1205
+ reusedExistingSession: reusedExistingCdpSession,
1206
+ executionWorkspace: workspace,
1207
+ configWorkspace,
1208
+ extensions: extensionIds,
1209
+ finalUrl: page.url(),
1210
+ title: await page.title(),
1211
+ actionResults
1212
+ };
1213
+ return JSON.stringify(summary, null, 2);
1214
+ } catch (error) {
1215
+ const message = error instanceof Error ? error.message : "Unknown browser error";
1216
+ logger.error(`browser_control failed: ${message}`);
1217
+ return `Error running browser_control: ${message}`;
1218
+ } finally{
1219
+ const contextToClose = launchedContext;
1220
+ if (contextToClose?.close) try {
1221
+ await contextToClose.close();
1222
+ } catch {}
1223
+ const browserToClose = browser;
1224
+ if (browserToClose) {
1225
+ if (!reusedExistingCdpSession) try {
1226
+ await browserToClose.close();
1227
+ } catch {}
1228
+ }
1229
+ const chromeSessionToClose = chromeSession;
1230
+ if (chromeSessionToClose) try {
1231
+ await chromeSessionToClose.close();
1232
+ } catch {}
1233
+ if (userDataDirSelection?.releaseLock) userDataDirSelection.releaseLock();
1234
+ if (userDataDirSelection && !userDataDirSelection.persistentProfile) deps.removeDir(userDataDirSelection.userDataDir);
1235
+ }
1236
+ }, {
1237
+ name: "browser_control",
1238
+ description: 'Native browser automation for Wingman using Chrome/Chromium runtime control. Transport is selected by config ("auto", "playwright", or "relay"): Playwright persistent-context is preferred for persistent profiles, CDP is used for standard runs with persistent-context fallback, and relay can bridge a live extension-attached tab. This is a first-class runtime capability, not an MCP server. Use it for JavaScript-rendered pages, interactions, screenshots, and structured extraction.',
1239
+ schema: BrowserControlInputSchema
1240
+ });
1241
+ };
1242
+ export { clearStaleDevtoolsArtifacts, createBrowserControlTool };