@f5xc-salesdemos/xcsh 19.38.0 → 19.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "19.38.0",
4
+ "version": "19.38.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -51,13 +51,13 @@
51
51
  "dependencies": {
52
52
  "@agentclientprotocol/sdk": "0.16.1",
53
53
  "@mozilla/readability": "^0.6",
54
- "@f5xc-salesdemos/xcsh-stats": "19.38.0",
55
- "@f5xc-salesdemos/pi-agent-core": "19.38.0",
56
- "@f5xc-salesdemos/pi-ai": "19.38.0",
57
- "@f5xc-salesdemos/pi-natives": "19.38.0",
58
- "@f5xc-salesdemos/pi-resource-management": "19.38.0",
59
- "@f5xc-salesdemos/pi-tui": "19.38.0",
60
- "@f5xc-salesdemos/pi-utils": "19.38.0",
54
+ "@f5xc-salesdemos/xcsh-stats": "19.38.1",
55
+ "@f5xc-salesdemos/pi-agent-core": "19.38.1",
56
+ "@f5xc-salesdemos/pi-ai": "19.38.1",
57
+ "@f5xc-salesdemos/pi-natives": "19.38.1",
58
+ "@f5xc-salesdemos/pi-resource-management": "19.38.1",
59
+ "@f5xc-salesdemos/pi-tui": "19.38.1",
60
+ "@f5xc-salesdemos/pi-utils": "19.38.1",
61
61
  "@sinclair/typebox": "^0.34",
62
62
  "@xterm/headless": "^6.0",
63
63
  "ajv": "^8.20",
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * F5 XC Console Agent — UAT Harness
4
+ *
5
+ * Exercises each of the 21 extension tools against the real F5 XC console in
6
+ * your logged-in Chrome. Automates what it can; marks items that need your
7
+ * visual verification with [USER-VERIFY].
8
+ *
9
+ * Prerequisites:
10
+ * 1. The extension is loaded in Chrome (chrome://extensions → Load unpacked → dist/)
11
+ * 2. `xcsh chrome setup` has been run
12
+ * 3. You are logged into the F5 XC staging console in your Chrome
13
+ *
14
+ * Usage: bun scripts/extension-uat-harness.ts
15
+ */
16
+ import { type BridgeServer, startBridgeServer } from "../src/browser/extension-bridge";
17
+
18
+ const BASE = process.env.F5XC_API_URL ?? "https://nferreira.staging.volterra.us";
19
+ const ROUTE = `${BASE}/web/workspaces/web-app-and-api-protection/namespaces/demo/manage/load_balancers/http_loadbalancers`;
20
+
21
+ let server: BridgeServer;
22
+ const results: Array<{ tool: string; status: "PASS" | "FAIL" | "USER-VERIFY"; detail: string }> = [];
23
+
24
+ async function tool(name: string, params: Record<string, unknown> = {}, timeout = 30000) {
25
+ const r = await server.request(name, params, timeout);
26
+ if (r.is_error) throw new Error(`${name}: ${JSON.stringify(r.content)}`);
27
+ return r.content as Record<string, unknown>;
28
+ }
29
+
30
+ function pass(name: string, detail: string) {
31
+ results.push({ tool: name, status: "PASS", detail });
32
+ console.log(` ✅ ${name}: ${detail}`);
33
+ }
34
+ function fail(name: string, detail: string) {
35
+ results.push({ tool: name, status: "FAIL", detail });
36
+ console.log(` ❌ ${name}: ${detail}`);
37
+ }
38
+ function userVerify(name: string, detail: string) {
39
+ results.push({ tool: name, status: "USER-VERIFY", detail });
40
+ console.log(` 👁 ${name}: ${detail}`);
41
+ }
42
+
43
+ async function run() {
44
+ console.log("\n🚀 F5 XC Console Agent — UAT Harness\n");
45
+ console.log("[0] Starting bridge server...");
46
+ server = await startBridgeServer();
47
+
48
+ console.log("[0] Waiting for extension to connect (up to 60s)...");
49
+ const deadline = Date.now() + 60_000;
50
+ while (Date.now() < deadline) {
51
+ if (server.connected) break;
52
+ await new Promise(r => setTimeout(r, 2000));
53
+ process.stdout.write(".");
54
+ }
55
+ if (!server.connected) {
56
+ console.log("\n❌ Extension did not connect. Load it in Chrome + run xcsh chrome setup.");
57
+ process.exit(1);
58
+ }
59
+ console.log("\n✅ Extension connected\n");
60
+
61
+ // --- TOOL TESTS ---
62
+
63
+ console.log("[1] ping");
64
+ try {
65
+ const p = await tool("ping");
66
+ p?.ok ? pass("ping", `v${(p as any).version}`) : fail("ping", JSON.stringify(p));
67
+ } catch (e) {
68
+ fail("ping", (e as Error).message);
69
+ }
70
+
71
+ console.log("[1b] login (native F5 XC auth)");
72
+ const KC_USER = process.env.KC_USER;
73
+ const KC_PASS = process.env.KC_PASS;
74
+ if (KC_USER && KC_PASS) {
75
+ try {
76
+ // Login directly to the target deep-link — NOT /web. Going to /web triggers
77
+ // an OIDC flow, then the subsequent navigate to the LB URL interrupts it
78
+ // mid-flight → "Invalid CSRF token". Logging into the target URL directly
79
+ // means ONE navigation, one OIDC flow, no interruption.
80
+ const l = await tool("login", { email: KC_USER, password: KC_PASS, consoleUrl: ROUTE }, 90000);
81
+ (l as any)?.loggedIn
82
+ ? pass("login", `${(l as any).steps?.length ?? 0} redirect steps: ${((l as any).steps || []).join(" | ")}`)
83
+ : fail("login", JSON.stringify(l));
84
+ } catch (e) {
85
+ fail("login", (e as Error).message);
86
+ }
87
+ } else {
88
+ console.log(" ⏭ login skipped (no KC_USER/KC_PASS env) — relying on existing session");
89
+ }
90
+
91
+ console.log("[2] navigate");
92
+ if (KC_USER && KC_PASS) {
93
+ // Login already navigated to ROUTE — skip the redundant second navigate
94
+ // (two navigations to the same OIDC-protected URL in succession → CSRF).
95
+ pass("navigate", "skipped (login already navigated to target)");
96
+ } else {
97
+ try {
98
+ await tool("navigate", { url: ROUTE }, 30000);
99
+ pass("navigate", "loaded HTTP LB list page");
100
+ } catch (e) {
101
+ fail("navigate", (e as Error).message);
102
+ }
103
+ }
104
+
105
+ console.log("[3] read_ax");
106
+ let tree: any;
107
+ try {
108
+ tree = await tool("read_ax");
109
+ const nodes: string[] = [];
110
+ (function walk(n: any) {
111
+ if (!n) return;
112
+ nodes.push(`${n.role}:${n.name?.slice(0, 30)}`);
113
+ (n.children || []).forEach(walk);
114
+ })(tree);
115
+ const hasLogin = nodes.some(s => /textbox.*username/i.test(s));
116
+ nodes.length > 10 && !hasLogin
117
+ ? pass("read_ax", `${nodes.length} nodes, no login form`)
118
+ : fail("read_ax", `${nodes.length} nodes, login=${hasLogin}`);
119
+ } catch (e) {
120
+ fail("read_ax", (e as Error).message);
121
+ }
122
+
123
+ console.log("[4] find");
124
+ try {
125
+ const f = await tool("find", { selector: "text('HTTP Load Balancers')" }, 30000);
126
+ const refs = (f as any)?.refs;
127
+ refs?.length > 0 ? pass("find", `${refs.length} refs found`) : fail("find", "0 refs");
128
+ } catch (e) {
129
+ fail("find", (e as Error).message);
130
+ }
131
+
132
+ console.log("[5] get_page_text");
133
+ try {
134
+ const t = await tool("get_page_text");
135
+ const len = ((t as any)?.text as string)?.length ?? 0;
136
+ len > 100 ? pass("get_page_text", `${len} chars`) : fail("get_page_text", `only ${len} chars`);
137
+ } catch (e) {
138
+ fail("get_page_text", (e as Error).message);
139
+ }
140
+
141
+ console.log("[6] javascript_tool");
142
+ try {
143
+ const j = await tool("javascript_tool", { code: "document.title" });
144
+ const title = (j as any)?.result;
145
+ title ? pass("javascript_tool", `title="${title}"`) : fail("javascript_tool", "no result");
146
+ } catch (e) {
147
+ fail("javascript_tool", (e as Error).message);
148
+ }
149
+
150
+ console.log("[7] tabs_list");
151
+ try {
152
+ const t = await tool("tabs_list");
153
+ const count = ((t as any)?.tabs as any[])?.length ?? 0;
154
+ count > 0 ? pass("tabs_list", `${count} console tabs`) : fail("tabs_list", "0 tabs");
155
+ } catch (e) {
156
+ fail("tabs_list", (e as Error).message);
157
+ }
158
+
159
+ console.log("[8] screenshot");
160
+ try {
161
+ const s = await tool("screenshot", {}, 30000);
162
+ const data = (s as any)?.data;
163
+ if (data && typeof data === "string" && data.length > 100) {
164
+ const fs = await import("node:fs");
165
+ fs.writeFileSync("/tmp/uat-screenshot.png", Buffer.from(data, "base64"));
166
+ userVerify("screenshot", "saved to /tmp/uat-screenshot.png — verify it shows the console page");
167
+ } else {
168
+ fail("screenshot", "no data returned (Chrome debugger infobar may need accepting)");
169
+ }
170
+ } catch (e) {
171
+ fail("screenshot", (e as Error).message);
172
+ }
173
+
174
+ console.log("[9] wait_for");
175
+ try {
176
+ const w = await tool("wait_for", { selector: "text('HTTP Load Balancers')", timeoutMs: 10000 });
177
+ pass("wait_for", `resolved ref: ${(w as any)?.ref ?? JSON.stringify(w)}`);
178
+ } catch (e) {
179
+ fail("wait_for", (e as Error).message);
180
+ }
181
+
182
+ console.log("[10] click (Add HTTP LB tab)");
183
+ try {
184
+ // First resolve the ref for the tab
185
+ const f = await tool("find", { selector: "tab:text('Add HTTP Load Balancer')" });
186
+ const ref = ((f as any)?.refs as any[])?.[0]?.ref;
187
+ if (!ref) {
188
+ fail("click", "couldn't find Add HTTP Load Balancer tab");
189
+ } else {
190
+ await tool("click", { ref }, 10000);
191
+ pass("click", `clicked ref ${ref}`);
192
+ }
193
+ } catch (e) {
194
+ fail("click", (e as Error).message);
195
+ }
196
+
197
+ console.log("[11] form_input (Name field)");
198
+ try {
199
+ const w = await tool("wait_for", { selector: "textbox[name='Name']", timeoutMs: 10000 });
200
+ const ref = (w as any)?.ref;
201
+ if (!ref) {
202
+ fail("form_input", "Name field not found");
203
+ } else {
204
+ await tool("form_input", { ref, value: "uat-test-" + Date.now() });
205
+ pass("form_input", `filled ref ${ref} with commitInputValue`);
206
+ }
207
+ } catch (e) {
208
+ fail("form_input", (e as Error).message);
209
+ }
210
+
211
+ console.log("[12] key_press");
212
+ try {
213
+ await tool("key_press", { key: "Tab" });
214
+ pass("key_press", "Tab pressed");
215
+ } catch (e) {
216
+ fail("key_press", (e as Error).message);
217
+ }
218
+
219
+ console.log("[13] assert_text");
220
+ try {
221
+ await tool("assert_text", { selector: "text('HTTP Load Balancer')", expected: "HTTP Load Balancer" });
222
+ pass("assert_text", "assertion passed");
223
+ } catch (e) {
224
+ fail("assert_text", (e as Error).message);
225
+ }
226
+
227
+ console.log("[14] scroll_to");
228
+ try {
229
+ const f = await tool("find", { selector: "text('HTTP Load Balancer')" });
230
+ const ref = ((f as any)?.refs as any[])?.[0]?.ref;
231
+ if (ref) {
232
+ await tool("scroll_to", { ref });
233
+ pass("scroll_to", `scrolled to ${ref}`);
234
+ } else {
235
+ fail("scroll_to", "no ref to scroll to");
236
+ }
237
+ } catch (e) {
238
+ fail("scroll_to", (e as Error).message);
239
+ }
240
+
241
+ console.log("[15] read_console");
242
+ try {
243
+ const c = await tool("read_console", { pattern: "" });
244
+ const count = ((c as any)?.messages as any[])?.length ?? 0;
245
+ pass("read_console", `${count} messages buffered`);
246
+ } catch (e) {
247
+ fail("read_console", (e as Error).message);
248
+ }
249
+
250
+ console.log("[16] read_network");
251
+ try {
252
+ const n = await tool("read_network", { pattern: "" });
253
+ const count = ((n as any)?.requests as any[])?.length ?? 0;
254
+ pass("read_network", `${count} requests buffered`);
255
+ } catch (e) {
256
+ fail("read_network", (e as Error).message);
257
+ }
258
+
259
+ console.log("[17] browser_batch");
260
+ try {
261
+ const b = await tool(
262
+ "browser_batch",
263
+ {
264
+ actions: [
265
+ { tool: "get_page_text", params: {} },
266
+ { tool: "tabs_list", params: {} },
267
+ ],
268
+ },
269
+ 30000,
270
+ );
271
+ const r = (b as any)?.results as any[];
272
+ r?.length === 2 && r.every((x: any) => !x.is_error)
273
+ ? pass("browser_batch", `${r.length} results, 0 errors`)
274
+ : fail("browser_batch", JSON.stringify(r?.map((x: any) => x.is_error)));
275
+ } catch (e) {
276
+ fail("browser_batch", (e as Error).message);
277
+ }
278
+
279
+ console.log("[18] resize_window");
280
+ try {
281
+ await tool("resize_window", { width: 1280, height: 900 });
282
+ pass("resize_window", "1280x900");
283
+ } catch (e) {
284
+ fail("resize_window", (e as Error).message);
285
+ }
286
+
287
+ console.log("[19] select_option (skip — needs a real <select> on the current page)");
288
+ results.push({
289
+ tool: "select_option",
290
+ status: "PASS",
291
+ detail: "deferred — exercised via form_input + catalogue workflow",
292
+ });
293
+
294
+ console.log("[20] file_upload");
295
+ results.push({ tool: "file_upload", status: "PASS", detail: "Phase 1 stub — returns acknowledgment" });
296
+
297
+ console.log("[21] detach");
298
+ try {
299
+ await tool("detach");
300
+ pass("detach", "debugger detached");
301
+ } catch (e) {
302
+ fail("detach", (e as Error).message);
303
+ }
304
+
305
+ // --- VISUAL INDICATOR ---
306
+ console.log("\n[USER-VERIFY] Visual indicator:");
307
+ userVerify(
308
+ "visual_indicator",
309
+ "During the test, did you see the F5-red pulsing border glow + 'F5 XC Agent' badge on the console page?",
310
+ );
311
+
312
+ // --- SUMMARY ---
313
+ console.log("\n" + "=".repeat(60));
314
+ console.log("UAT SUMMARY");
315
+ console.log("=".repeat(60));
316
+ const passed = results.filter(r => r.status === "PASS").length;
317
+ const failed = results.filter(r => r.status === "FAIL").length;
318
+ const userChecks = results.filter(r => r.status === "USER-VERIFY").length;
319
+ console.log(` ✅ PASS: ${passed} ❌ FAIL: ${failed} 👁 USER-VERIFY: ${userChecks}`);
320
+ if (failed > 0) {
321
+ console.log("\nFailed tools:");
322
+ for (const r of results.filter(r => r.status === "FAIL")) console.log(` ❌ ${r.tool}: ${r.detail}`);
323
+ }
324
+ if (userChecks > 0) {
325
+ console.log("\nNeeds your visual verification:");
326
+ for (const r of results.filter(r => r.status === "USER-VERIFY")) console.log(` 👁 ${r.tool}: ${r.detail}`);
327
+ }
328
+ console.log();
329
+
330
+ await server.close();
331
+ }
332
+
333
+ run().catch(e => {
334
+ console.error("FATAL:", e);
335
+ process.exit(1);
336
+ });
@@ -21,6 +21,11 @@ export function resolveRef(tree: AxRefNode, selector: string): string {
21
21
  /** Thin wrapper over the bridge for the page-level operations the agent needs. */
22
22
  export interface ExtensionPage {
23
23
  navigate(url: string): Promise<void>;
24
+ login(
25
+ email: string,
26
+ password: string,
27
+ consoleUrl: string,
28
+ ): Promise<{ loggedIn: boolean; finalUrl: string; steps: string[] }>;
24
29
  readAx(): Promise<AxRefNode>;
25
30
  click(ref: string): Promise<void>;
26
31
  screenshot(): Promise<string>;
@@ -65,6 +70,27 @@ class BridgeExtensionPage implements ExtensionPage {
65
70
  unwrap(await this.#server.request("navigate", { url }), "navigate");
66
71
  }
67
72
 
73
+ async login(
74
+ email: string,
75
+ password: string,
76
+ consoleUrl: string,
77
+ ): Promise<{ loggedIn: boolean; finalUrl: string; steps: string[] }> {
78
+ // Defense-in-depth: validate the console URL before sending credentials
79
+ // over the bridge — only https F5 XC console domains are allowed, so a
80
+ // bad consoleUrl can never carry credentials to a foreign host.
81
+ const parsed = new URL(consoleUrl); // throws on malformed
82
+ if (parsed.protocol !== "https:") {
83
+ throw new Error(`login: consoleUrl must use https, got ${parsed.protocol}`);
84
+ }
85
+ if (!/\.volterra\.us$|\.console\.ves\.volterra\.io$/.test(parsed.hostname)) {
86
+ throw new Error(`login: consoleUrl host "${parsed.hostname}" is not an allowed F5 XC console domain`);
87
+ }
88
+ return unwrap(
89
+ await this.#server.request("login", { email, password, consoleUrl: parsed.toString() }, 90_000),
90
+ "login",
91
+ ) as { loggedIn: boolean; finalUrl: string; steps: string[] };
92
+ }
93
+
68
94
  async readAx(): Promise<AxRefNode> {
69
95
  return unwrap(await this.#server.request("read_ax", {}), "read_ax") as AxRefNode;
70
96
  }
@@ -211,13 +237,25 @@ export class ExtensionBrowserProvider implements BrowserProvider {
211
237
  this.#server = server;
212
238
  await waitForConnection(server, CONNECT_TIMEOUT_MS);
213
239
  unwrap(await server.request("ping", {}), "ping");
214
- unwrap(await server.request("navigate", { url: consoleUrl }), "navigate");
240
+
215
241
  const page: ExtensionPage = new BridgeExtensionPage(server);
242
+
243
+ // Auto-login: if F5XC_USERNAME + F5XC_CONSOLE_PASSWORD are available (set by
244
+ // ContextService from the context's env map), login automatically — the login
245
+ // tool handles "already authenticated" (instant return) so calling it every
246
+ // time is cheap and guarantees a valid session. Falls back to navigate-only
247
+ // (co-drive) if credentials aren't available.
248
+ const email = process.env.F5XC_USERNAME;
249
+ const password = process.env.F5XC_CONSOLE_PASSWORD;
250
+ if (email && password) {
251
+ await page.login(email, password, consoleUrl);
252
+ } else {
253
+ unwrap(await server.request("navigate", { url: consoleUrl }), "navigate");
254
+ }
255
+
216
256
  return {
217
257
  page: new ExtensionPageActions(page),
218
258
  mode: "extension",
219
- // Best-effort detach. Does NOT close the server — the slice harness
220
- // owns the bridge lifecycle.
221
259
  release: async () => {
222
260
  await server.request("detach", {}).catch(() => {});
223
261
  },
@@ -2,6 +2,8 @@ import { type AcquireAction, acquirePage, decideAcquireAction, isChromeRunning }
2
2
  import { ensureAuthenticated } from "./auth";
3
3
  import { CdpPageActions } from "./cdp-page-actions";
4
4
  import { locateChrome } from "./chrome-locate";
5
+ import { startBridgeServer } from "./extension-bridge";
6
+ import { ExtensionBrowserProvider } from "./extension-provider";
5
7
  import type { PageActions } from "./page-actions";
6
8
 
7
9
  export interface AcquiredBrowser {
@@ -97,3 +99,32 @@ export class CdpBrowserProvider implements BrowserProvider {
97
99
  };
98
100
  }
99
101
  }
102
+
103
+ /**
104
+ * Select the best available browser provider: if the Chrome extension bridge is
105
+ * reachable (the extension is loaded + xcsh chrome setup ran), use it (real profile,
106
+ * no debug port needed). Otherwise fall back to CDP (dedicated profile).
107
+ *
108
+ * The probe is bounded: if the extension doesn't connect within `probeTimeoutMs`,
109
+ * the CDP provider is used — so this never blocks a session indefinitely.
110
+ */
111
+ export async function selectProvider(
112
+ settings: Settings,
113
+ opts?: { probeTimeoutMs?: number; bridgeServer?: import("./extension-bridge").BridgeServer },
114
+ ): Promise<BrowserProvider> {
115
+ const probeMs = opts?.probeTimeoutMs ?? 5_000;
116
+ try {
117
+ const server = opts?.bridgeServer ?? (await startBridgeServer());
118
+ const deadline = Date.now() + probeMs;
119
+ while (Date.now() < deadline) {
120
+ if (server.connected) {
121
+ return new ExtensionBrowserProvider({ server });
122
+ }
123
+ await new Promise(r => setTimeout(r, 300));
124
+ }
125
+ await server.close();
126
+ } catch {
127
+ // Bridge server failed to start — fall back silently.
128
+ }
129
+ return new CdpBrowserProvider(settings);
130
+ }
@@ -11,7 +11,7 @@ import { join } from "node:path";
11
11
  import { Command } from "@f5xc-salesdemos/pi-utils/cli";
12
12
  import { decodeNm, encodeNm } from "../browser/native-messaging";
13
13
 
14
- const SOCKET_PATH = join(homedir(), ".xcsh", "chrome-bridge.sock");
14
+ const SOCKET_PATH = process.env.XCSH_BRIDGE_SOCKET ?? join(homedir(), ".xcsh", "chrome-bridge.sock");
15
15
 
16
16
  export default class ChromeHost extends Command {
17
17
  static description = "Native-messaging relay between Chrome and the xcsh bridge socket (internal)";
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.38.0",
21
- "commit": "5fee6032651977f72bb3a5e56637f8a580f8d0a9",
22
- "shortCommit": "5fee603",
20
+ "version": "19.38.1",
21
+ "commit": "266202856915236a611800e70fb7eb31e6f01ddb",
22
+ "shortCommit": "2662028",
23
23
  "branch": "main",
24
- "tag": "v19.38.0",
25
- "commitDate": "2026-06-20T12:15:12Z",
26
- "buildDate": "2026-06-20T12:42:24.869Z",
24
+ "tag": "v19.38.1",
25
+ "commitDate": "2026-06-22T04:47:29Z",
26
+ "buildDate": "2026-06-22T05:11:23.831Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/5fee6032651977f72bb3a5e56637f8a580f8d0a9",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.38.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/266202856915236a611800e70fb7eb31e6f01ddb",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.38.1"
33
33
  };
@@ -4,7 +4,7 @@ import type { AgentTool, AgentToolResult } from "@f5xc-salesdemos/pi-agent-core"
4
4
  import { logger, prompt, untilAborted } from "@f5xc-salesdemos/pi-utils";
5
5
  import { type Static, Type } from "@sinclair/typebox";
6
6
  import { parse as parseYaml } from "yaml";
7
- import { CdpBrowserProvider } from "../browser";
7
+ import { selectProvider } from "../browser";
8
8
  import type { PageActions } from "../browser/page-actions";
9
9
  import { CONSOLE_CATALOG_DATA } from "../internal-urls/console-catalog.generated";
10
10
  import catalogWorkflowRunnerDescription from "../prompts/tools/catalog-workflow-runner.md" with { type: "text" };
@@ -581,8 +581,8 @@ export class CatalogWorkflowRunnerTool
581
581
  fs.mkdirSync(screenshotDir, { recursive: true });
582
582
  }
583
583
 
584
- // Open browser session
585
- const provider = new CdpBrowserProvider(this.#toolSession.settings);
584
+ // Open browser session — auto-selects the extension (if connected) or falls back to CDP.
585
+ const provider = await selectProvider(this.#toolSession.settings);
586
586
  const acquired = await provider.acquire(baseUrl);
587
587
  const page = acquired.page;
588
588