diff options
| author | amankumarsingh77 <amankumarsingh7702@gmal.com> | 2026-05-02 16:36:50 +0530 |
|---|---|---|
| committer | amankumarsingh77 <amankumarsingh7702@gmal.com> | 2026-05-02 16:36:50 +0530 |
| commit | b7fd72a6aa065df6335d1b4710c07bb4fd67615e (patch) | |
| tree | 4786b3297e7a4feae27abf27c7734b7661db6b4f | |
| parent | 96934b772378b57aed243919d3c1d6ad6e96df1f (diff) | |
feat(domains): migrate http_get (timeout covers body) and get_network_log (deprecation note)
| -rw-r--r-- | src/domains/network.ts | 80 | ||||
| -rw-r--r-- | src/registry.ts | 3 | ||||
| -rw-r--r-- | src/tools.ts | 179 |
3 files changed, 83 insertions, 179 deletions
diff --git a/src/domains/network.ts b/src/domains/network.ts new file mode 100644 index 0000000..312f39a --- /dev/null +++ b/src/domains/network.ts @@ -0,0 +1,80 @@ +import { Type } from "typebox"; +import { type Result, err, ok } from "../util/result"; +import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; +import { applyTruncation } from "../util/truncate"; + +const HttpGetArgs = Type.Object({ + url: Type.String({ description: "URL to GET" }), + headers: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Optional HTTP headers" })), + timeout: Type.Optional( + Type.Number({ default: 20, minimum: 1, maximum: 120, description: "Total seconds (covers headers AND body)" }), + ), +}); + +export const httpGetTool = defineBrowserTool({ + name: "browser_http_get", + label: "Browser HTTP GET", + description: + "Fetch a URL outside the browser (faster than browser_navigate for APIs and static pages). Timeout covers headers AND body read.", + promptSnippet: "HTTP GET (outside browser; for APIs/static pages)", + promptGuidelines: [ + "Faster than navigate+execute_js for JSON/HTML APIs.", + "No JS rendering — for SPAs use browser_navigate.", + ], + parameters: HttpGetArgs, + ensureAlive: false, + async handler(args): Promise<Result<ToolOk, ToolErr>> { + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), (args.timeout ?? 20) * 1000); + try { + const fetchInit: RequestInit = { signal: ac.signal }; + if (args.headers !== undefined) fetchInit.headers = args.headers; + const res = await fetch(args.url, fetchInit); + const body = await res.text(); + clearTimeout(timer); + const ct = res.headers.get("content-type") ?? ""; + const truncated = await applyTruncation(body, "http"); + return ok({ + text: `HTTP ${res.status} ${ct}\n${truncated.text}`, + details: truncated.fullOutputPath !== undefined + ? { status: res.status, contentType: ct, length: body.length, fullOutputPath: truncated.fullOutputPath, wasTruncated: truncated.wasTruncated } + : { status: res.status, contentType: ct, length: body.length, wasTruncated: truncated.wasTruncated }, + }); + } catch (e) { + clearTimeout(timer); + const msg = e instanceof Error ? e.message : String(e); + const kind: ToolErr["kind"] = msg.toLowerCase().includes("abort") ? "timeout" : "io_error"; + return err({ kind, message: msg }); + } + }, +}); + +const NetLogArgs = Type.Object({ + eventTypes: Type.Optional( + Type.Array(Type.String(), { description: "CDP event names to filter (e.g., ['Network.requestWillBeSent'])" }), + ), + limit: Type.Optional(Type.Integer({ default: 50, minimum: 1, maximum: 500 })), +}); + +export const getNetworkLogTool = defineBrowserTool({ + name: "browser_get_network_log", + 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)", + 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.", + ], + parameters: NetLogArgs, + async handler(args): Promise<Result<ToolOk, ToolErr>> { + return ok({ + text: "browser_get_network_log: in v0.3 the CDP event stream is consumed by the session; use browser_execute_js with PerformanceObserver or performance.getEntries() to inspect network activity.", + details: { + deprecated: true, + eventTypes: args.eventTypes ?? null, + limit: args.limit ?? null, + }, + }); + }, +}); diff --git a/src/registry.ts b/src/registry.ts index 06ec74b..892d656 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -13,6 +13,7 @@ import { listTabsTool, currentTabTool, switchTabTool, newTabTool } from "./domai import { uploadFileTool, downloadTool, printToPdfTool } from "./domains/files"; import { viewportResizeTool } from "./domains/viewport"; import { dragAndDropTool } from "./domains/drag"; +import { httpGetTool, getNetworkLogTool } from "./domains/network"; const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [ clickTool, @@ -39,6 +40,8 @@ const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [ printToPdfTool, viewportResizeTool, dragAndDropTool, + httpGetTool, + getNetworkLogTool, ]; export const registerAllTools = (pi: ExtensionAPI, client: BrowserClient): void => { diff --git a/src/tools.ts b/src/tools.ts index f665287..b1d27a9 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -168,185 +168,6 @@ export function registerTools(pi: ExtensionAPI, daemon: BrowserDaemon): void { }); // ═══════════════════════════════════════════════════════════════════════════ - // browser_http_get - // ═══════════════════════════════════════════════════════════════════════════ - pi.registerTool({ - name: "browser_http_get", - label: "Browser HTTP GET", - description: - "Make a direct HTTP GET request (no browser). Use for APIs and static pages — much faster than browser navigation for data retrieval.", - promptSnippet: "Make a direct HTTP GET request (outside browser, for APIs)", - promptGuidelines: [ - "Use browser_http_get for API calls and static page retrieval — it bypasses the browser entirely and is 10-50x faster.", - "Use browser_http_get with ThreadPoolExecutor for bulk data retrieval from multiple URLs.", - "Do NOT use browser_http_get for pages that require JavaScript rendering — use browser_navigate + browser_execute_js instead.", - ], - parameters: Type.Object({ - url: Type.String({ description: "URL to fetch" }), - headers: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Optional HTTP headers" })), - timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 20" })), - }), - async execute(_id, params) { - try { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), (params.timeout || 20) * 1000); - - const response = await fetch(params.url, { - headers: { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", - Accept: "text/html,application/json,*/*", - ...params.headers, - }, - signal: controller.signal, - }); - - clearTimeout(timer); - - const body = await response.text(); - const contentType = response.headers.get("content-type") || "unknown"; - - // Build header (always shown) and truncate body separately - const headerText = `HTTP ${response.status} ${response.statusText}\nContent-Type: ${contentType}\nLength: ${body.length} chars\n\n`; - const { text: truncatedBody, fullOutputPath, wasTruncated } = await applyTruncation( - body, - "http", - ); - - return { - content: [{ - type: "text" as const, - text: headerText + truncatedBody, - }], - details: { - status: response.status, - contentType, - length: body.length, - fullOutputPath, - wasTruncated, - }, - }; - } catch (err) { - return { - isError: true, - content: [{ type: "text" as const, text: `HTTP request failed: ${err instanceof Error ? err.message : String(err)}` }], - details: undefined, - }; - } - }, - renderResult(result, { expanded }, theme) { - const details = result.details as { - status?: number; - contentType?: string; - length?: number; - wasTruncated?: boolean; - fullOutputPath?: string; - } | undefined; - - const status = details?.status ?? 0; - const ct = details?.contentType ?? "unknown"; - const len = details?.length ?? 0; - const wasTruncated = details?.wasTruncated ?? false; - - if (expanded) { - // Expanded (CTRL+O): show full response body - const content = result.content[0]; - const raw = content?.type === "text" ? content.text : ""; - return new Text(raw || "(empty response)", 0, 0); - } - - // Collapsed (default): compact HTTP response summary - const shortCt = ct.split(";")[0] || ct; // drop charset etc. - const truncNote = wasTruncated ? " (truncated)" : ""; - const summary = `🌐 HTTP ${status}, ${shortCt}, ${formatSize(len)}${truncNote}`; - - return new Text( - theme - ? theme.fg(status >= 200 && status < 300 ? "success" : status >= 400 ? "error" : "warning", summary) - : summary, - 0, - 0, - ); - }, - }); - - // ═══════════════════════════════════════════════════════════════════════════ - // browser_get_network_log - // ═══════════════════════════════════════════════════════════════════════════ - pi.registerTool({ - name: "browser_get_network_log", - label: "Browser Get Network Log", - description: - "Get buffered network events (Network.requestWillBeSent, Network.responseReceived) " + - "from the current page. Useful for inspecting XHR/fetch calls the page makes, " + - "finding API endpoints, or debugging network issues.", - promptSnippet: "Get buffered network request/response events", - promptGuidelines: [ - "Use browser_get_network_log to find API endpoints the page calls — often 10x faster than DOM scraping.", - "Network events are buffered since the last call to this tool — call it after performing actions that trigger XHR/fetch.", - "Filter by eventType to focus on specific events: 'Network.requestWillBeSent' (URLs, headers) or 'Network.responseReceived' (status, response headers).", - "Combine with browser_http_get to replay captured API calls.", - ], - parameters: Type.Object({ - eventTypes: Type.Optional( - Type.Array(Type.String(), { - description: - "Network event types to filter. Default: ['Network.requestWillBeSent', 'Network.responseReceived']", - }), - ), - limit: Type.Optional(Type.Number({ description: "Max events to return. Default: 50" })), - }), - async execute(_id, params) { - try { - await daemon.ensureAlive(); - const events = await daemon.getNetworkLog( - params.eventTypes, - params.limit, - ); - - if (events.length === 0) { - return { - content: [{ type: "text" as const, text: "No buffered network events. Perform actions that trigger XHR/fetch calls first." }], - details: undefined, - }; - } - - const lines = events.map((e) => { - const p = e.params as Record<string, unknown>; - if (e.method === "Network.requestWillBeSent") { - const req = (p.request as Record<string, unknown>) || {}; - return [ - `→ ${req.method || "GET"} ${req.url || p.documentURL || "?"}`, - ` Type: ${p.type || "?"}`, - ].join("\n"); - } - if (e.method === "Network.responseReceived") { - const resp = (p.response as Record<string, unknown>) || {}; - return [ - `← ${resp.status || "?"} ${resp.statusText || ""} ${resp.url || p.url || "?"}`, - ` MIME: ${resp.mimeType || "?"}`, - ].join("\n"); - } - return `${e.method}: ${JSON.stringify(p).slice(0, 200)}`; - }); - - const raw = `${events.length} network event(s):\n\n${lines.join("\n\n")}`; - const { text, fullOutputPath } = await applyTruncation(raw, "net"); - - return { - content: [{ type: "text" as const, text }], - details: { count: events.length, fullOutputPath }, - }; - } catch (err) { - return { - isError: true, - content: [{ type: "text" as const, text: `Network log failed: ${err instanceof Error ? err.message : String(err)}` }], - details: undefined, - }; - } - }, - }); - - // ═══════════════════════════════════════════════════════════════════════════ // browser_run_script // ═══════════════════════════════════════════════════════════════════════════ pi.registerTool({ |
