diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client.ts | 24 | ||||
| -rw-r--r-- | src/domains/files.ts | 10 | ||||
| -rw-r--r-- | src/domains/keyboard.ts | 1 | ||||
| -rw-r--r-- | src/domains/navigate.ts | 1 | ||||
| -rw-r--r-- | src/domains/network.ts | 2 | ||||
| -rw-r--r-- | src/domains/tabs.ts | 42 |
6 files changed, 68 insertions, 12 deletions
diff --git a/src/client.ts b/src/client.ts index 51dfa9f..1bf7ccc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -108,12 +108,26 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient = } if (Date.now() - lastHealth < HEALTH_TTL_MS) return ok(undefined); const probe = await transport.request("Target.getTargets", {}, { sessionId: null, timeoutMs: 2_000 }); - if (probe.success) { - lastHealth = Date.now(); - return ok(undefined); + if (!probe.success) { + await stop(); + return start(); } - await stop(); - return start(); + lastHealth = Date.now(); + // Verify the page session is still responsive (handles the case where the + // browser transport is alive but the page target crashed, e.g. localhost died). + const jsProbe = await session.call("Runtime.evaluate", { + expression: "1", returnByValue: true, + }, { timeoutMs: 2_000 }); + if (!jsProbe.success && jsProbe.error.kind === "session_not_found") { + const reattached = await session.attachFirstPage(); + if (reattached.success) { + pageCache = null; + return ok(undefined); + } + await stop(); + return start(); + } + return ok(undefined); }; const evaluateJs = async (expression: string, sessionId?: string): Promise<Result<unknown, CdpError>> => { diff --git a/src/domains/files.ts b/src/domains/files.ts index f0cb6b0..91f8761 100644 --- a/src/domains/files.ts +++ b/src/domains/files.ts @@ -1,4 +1,4 @@ -import { access, constants, stat, readFile, writeFile } from "node:fs/promises"; +import { mkdir, access, constants, stat, readFile, writeFile } from "node:fs/promises"; import { basename } from "node:path"; import { Type } from "typebox"; import type { BrowserClient } from "../client"; @@ -136,6 +136,14 @@ export const downloadTool = defineBrowserTool({ parameters: DownloadArgs, async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> { try { + await mkdir(args.downloadPath, { recursive: true }); + } catch (e) { + return err({ + kind: "io_error", + message: `Cannot create download directory: ${args.downloadPath} (${e instanceof Error ? e.message : String(e)})`, + }); + } + try { const s = await stat(args.downloadPath); if (!s.isDirectory()) { return err({ kind: "io_error", message: `Not a directory: ${args.downloadPath}` }); diff --git a/src/domains/keyboard.ts b/src/domains/keyboard.ts index d953b76..c39814e 100644 --- a/src/domains/keyboard.ts +++ b/src/domains/keyboard.ts @@ -91,6 +91,7 @@ export const dispatchKeyTool = defineBrowserTool({ "Dispatch a DOM KeyboardEvent on a specific element via JS injection. Use for React/Vue components that listen to synthetic events more reliably than CDP input.", promptSnippet: "Dispatch a DOM KeyboardEvent on a specific element", promptGuidelines: [ + "Dispatches a synthetic DOM KeyboardEvent — for React/Vue synthetic event listeners. Does NOT insert text into inputs (use browser_type or browser_press_key for actual typing).", "Try browser_press_key first; only use browser_dispatch_key when the page ignores raw CDP key events.", "The selector must match exactly one or more elements; zero matches is reported as an error.", "eventType defaults to 'keydown'.", diff --git a/src/domains/navigate.ts b/src/domains/navigate.ts index 5a59629..6016122 100644 --- a/src/domains/navigate.ts +++ b/src/domains/navigate.ts @@ -22,6 +22,7 @@ export const navigateTool = defineBrowserTool({ "Use browser_navigate to go to URLs.", "Use browser_wait_for_load after browser_navigate to wait for the page to finish loading.", "For extracting data from a page you already navigated to, use browser_execute_js or browser_http_get (faster for APIs).", + "Note: Google and some sites with strict anti-bot detection may reject CDP navigation. Use browser_http_get for search results.", ], parameters: NavigateArgs, renderCall: (a) => new Text(`🌐 Navigate to ${a.url}`, 0, 0), diff --git a/src/domains/network.ts b/src/domains/network.ts index 312f39a..1377486 100644 --- a/src/domains/network.ts +++ b/src/domains/network.ts @@ -61,7 +61,7 @@ export const getNetworkLogTool = defineBrowserTool({ label: "Browser Get Network Log", description: "Read buffered Network.* CDP events. NOTE: in v0.3 the event stream is consumed internally by the session manager; this tool is a placeholder. Use browser_execute_js with PerformanceObserver as a workaround.", - promptSnippet: "Get buffered network events (DEPRECATED in v0.3 — see description)", + promptSnippet: "Get buffered network events (DEPRECATED in v0.3 — use browser_execute_js with PerformanceObserver instead)", promptGuidelines: [ "v0.3 routes CDP events through an internal AsyncIterable; a synchronous drain API isn't exposed yet.", "For now, use browser_execute_js with PerformanceObserver, performance.getEntries(), or fetch interceptors.", diff --git a/src/domains/tabs.ts b/src/domains/tabs.ts index c9440fe..d2cd646 100644 --- a/src/domains/tabs.ts +++ b/src/domains/tabs.ts @@ -12,14 +12,15 @@ export const listTabsTool = defineBrowserTool({ description: "List all open browser tabs (page targets).", promptSnippet: "List browser tabs", promptGuidelines: [ - "Use to find a targetId for browser_switch_tab.", + "Use to find a full targetId for browser_switch_tab.", "Internal tabs (chrome://) included by default; pass includeInternal=false to exclude them.", + "Each tab shows its full 32-char hex targetId — use the exact value with browser_switch_tab.", ], parameters: ListTabsArgs, async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> { const r = await client.listTabs(args.includeInternal ?? true); if (!r.success) return err({ kind: "cdp_error", message: r.error.message }); - const lines = r.data.map((t, i) => ` [${i}] ${t.targetId.slice(0, 8)}… ${t.url}\n ${t.title}`); + const lines = r.data.map((t, i) => ` [${i}] ${t.targetId} ${t.url}\n ${t.title}`); return ok({ text: `Tabs (${r.data.length}):\n${lines.join("\n")}`, details: { tabs: r.data }, @@ -56,12 +57,43 @@ export const switchTabTool = defineBrowserTool({ label: "Browser Switch Tab", description: "Switch to and attach to a different tab by targetId.", promptSnippet: "Switch tabs", - promptGuidelines: ["Get a targetId via browser_list_tabs first."], + promptGuidelines: [ + "Get a targetId via browser_list_tabs first.", + "Accepts exact targetId or a unique prefix of at least 8 hex characters.", + ], parameters: SwitchTabArgs, async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> { - const r = await client.switchTab(args.targetId); + // Try exact match first + let r = await client.switchTab(args.targetId); + if (r.success) { + return ok({ text: `Switched to ${args.targetId}`, details: { targetId: args.targetId } }); + } + // If exact match failed and input looks like a short prefix (≥8 hex chars), try prefix resolution + const isHexPrefix = /^[0-9A-Fa-f]{8,}$/.test(args.targetId); + if (!isHexPrefix) { + return err({ kind: "cdp_error", message: r.error.message }); + } + const tabs = await client.listTabs(true); + if (!tabs.success) return err({ kind: "cdp_error", message: r.error.message }); + const matches = tabs.data.filter((t) => t.targetId.startsWith(args.targetId)); + if (matches.length === 0) { + return err({ kind: "cdp_error", message: `No tab found with prefix "${args.targetId}"` }); + } + if (matches.length > 1) { + const ids = matches.map((t) => t.targetId).join(", "); + return err({ + kind: "invalid_state", + message: `Ambiguous prefix "${args.targetId}" matches ${matches.length} tabs: ${ids}`, + details: { matches: matches.map((t) => ({ targetId: t.targetId, url: t.url })) }, + }); + } + const resolved = matches[0]!.targetId; + r = await client.switchTab(resolved); if (!r.success) return err({ kind: "cdp_error", message: r.error.message }); - return ok({ text: `Switched to ${args.targetId}`, details: { targetId: args.targetId } }); + return ok({ + text: `Switched to ${resolved} (resolved from prefix "${args.targetId}")`, + details: { targetId: resolved, resolvedPrefix: args.targetId }, + }); }, }); |
