mirror of
https://github.com/runablehq/mini-browser.git
synced 2026-09-20 13:45:46 +08:00
feat tabId
This commit is contained in:
@@ -85,7 +85,7 @@ Recording:
|
||||
record status Check recording status
|
||||
|
||||
Tabs:
|
||||
tab list / new [url] / close [n]
|
||||
tab list / new [url] / close [id]
|
||||
|
||||
Other:
|
||||
js <code> Eval JS in page — strings print raw, objects print JSON
|
||||
@@ -95,7 +95,7 @@ Other:
|
||||
|
||||
Flags:
|
||||
--timeout <ms> (default: 30000)
|
||||
--tab <n> target tab (default: 0)
|
||||
--tab <id> target tab by stable Chrome id (default: first page)
|
||||
--json structured output (snap, tab list, logs, audit)
|
||||
--right right-click
|
||||
--double double-click
|
||||
@@ -182,15 +182,25 @@ output.
|
||||
|
||||
### Tabs
|
||||
|
||||
`tab list` prints index, URL, and title for each open tab.
|
||||
`tab new [url]` opens a new tab (optionally navigating) and prints its index.
|
||||
`tab close [n]` closes a tab by index (default: last). Cannot close the last
|
||||
remaining tab.
|
||||
Every tab has a stable id (Chrome's CDP `targetId`) that survives other tabs
|
||||
opening or closing. Use `--tab <id>` to target a specific tab from any
|
||||
command. Without `--tab`, commands operate on the first open page.
|
||||
|
||||
`tab list` prints id, URL, and title for each open tab.
|
||||
`tab new [url]` opens a new tab (optionally navigating) and prints its id.
|
||||
`tab close [id]` closes a tab by id (default: last opened). Cannot close the
|
||||
last remaining tab.
|
||||
|
||||
```bash
|
||||
ID=$(mb tab new https://example.com)
|
||||
mb --tab "$ID" shot example.png
|
||||
mb tab close "$ID"
|
||||
```
|
||||
|
||||
### JSON output
|
||||
|
||||
`--json` on `snap` returns `[{role, name, x, y, state}]`.
|
||||
`--json` on `tab list` returns `[{index, url, title}]`.
|
||||
`--json` on `tab list` returns `[{id, url, title}]`.
|
||||
`--json` on `logs` emits JSON lines `{tab, type, time, message}`.
|
||||
`--json` on `audit` returns the full audit data as a JSON object.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@runablehq/mini-browser",
|
||||
"version": "0.7.1",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "A browser CLI for agents, built with Puppeteer",
|
||||
"keywords": [
|
||||
|
||||
@@ -21,7 +21,7 @@ const getDaemonPath = () => {
|
||||
|
||||
interface StartInput {
|
||||
path: string
|
||||
tab: number
|
||||
tab?: string
|
||||
fps: number
|
||||
scale: number
|
||||
}
|
||||
|
||||
+45
-23
@@ -1,14 +1,15 @@
|
||||
import puppeteer from "puppeteer-core"
|
||||
import { CDP_URL, VIEWPORT } from "../lib/config"
|
||||
import { targetId } from "../lib/browser"
|
||||
import type { Flags } from "../lib/flags"
|
||||
|
||||
const subcommands = { list, new: newTab, close }
|
||||
|
||||
const help = `Usage: mb tab <list|new|close> [args]
|
||||
|
||||
list List open tabs
|
||||
new [url] Open new tab, print index
|
||||
close [n] Close tab (default: last)`
|
||||
list List open tabs (id, url, title)
|
||||
new [url] Open new tab, print id
|
||||
close [id] Close tab by id (default: last opened)`
|
||||
|
||||
export const tab = async (args: string[], flags: Flags) => {
|
||||
const [sub, ...rest] = args
|
||||
@@ -24,14 +25,17 @@ export const tab = async (args: string[], flags: Flags) => {
|
||||
|
||||
async function list(_args: string[], flags: Flags) {
|
||||
const browser = await puppeteer.connect({ browserURL: CDP_URL, defaultViewport: VIEWPORT })
|
||||
const pages = await browser.pages()
|
||||
const targets = browser.targets().filter((t) => t.type() === "page")
|
||||
|
||||
const entries = await Promise.all(
|
||||
pages.map(async (page, i) => ({
|
||||
index: i,
|
||||
url: page.url(),
|
||||
title: await page.title(),
|
||||
}))
|
||||
targets.map(async (target) => {
|
||||
const page = await target.page()
|
||||
return {
|
||||
id: targetId(target),
|
||||
url: target.url(),
|
||||
title: page ? await page.title() : "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
await browser.disconnect()
|
||||
@@ -41,15 +45,29 @@ async function list(_args: string[], flags: Flags) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const { index, url, title } of entries) {
|
||||
console.log(`${index}\t${url}\t${title}`)
|
||||
for (const { id, url, title } of entries) {
|
||||
console.log(`${id}\t${url}\t${title}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function newTab(args: string[], flags: Flags) {
|
||||
const url = args[0]
|
||||
const browser = await puppeteer.connect({ browserURL: CDP_URL, defaultViewport: VIEWPORT })
|
||||
const page = await browser.newPage()
|
||||
|
||||
/* page.tagrte is deprecated in puppeteer-core,
|
||||
so we use the CDP Target.createTarget directly to get the targetId */
|
||||
|
||||
const cdp = await browser.target().createCDPSession()
|
||||
const { targetId: id } = await cdp.send("Target.createTarget", { url: "about:blank" })
|
||||
await cdp.detach()
|
||||
|
||||
const target = await browser.waitForTarget((t) => targetId(t) === id)
|
||||
const page = await target.page()
|
||||
|
||||
if (!page) {
|
||||
await browser.disconnect()
|
||||
throw new Error("Failed to attach to new tab")
|
||||
}
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
@@ -61,31 +79,35 @@ async function newTab(args: string[], flags: Flags) {
|
||||
}
|
||||
}
|
||||
|
||||
const pages = await browser.pages()
|
||||
const index = pages.indexOf(page)
|
||||
await browser.disconnect()
|
||||
console.log(index)
|
||||
console.log(id)
|
||||
}
|
||||
|
||||
async function close(args: string[], _flags: Flags) {
|
||||
const browser = await puppeteer.connect({ browserURL: CDP_URL, defaultViewport: VIEWPORT })
|
||||
const pages = await browser.pages()
|
||||
const targets = browser.targets().filter((t) => t.type() === "page")
|
||||
|
||||
if (pages.length <= 1) {
|
||||
if (targets.length <= 1) {
|
||||
await browser.disconnect()
|
||||
throw new Error("Cannot close the last tab")
|
||||
}
|
||||
|
||||
const index = args[0] !== undefined ? Number(args[0]) : pages.length - 1
|
||||
const requestedId = args[0]
|
||||
|
||||
if (!Number.isInteger(index) || index < 0 || index >= pages.length) {
|
||||
const target = requestedId
|
||||
? targets.find((t) => targetId(t) === requestedId)
|
||||
: targets[targets.length - 1]
|
||||
|
||||
if (!target) {
|
||||
await browser.disconnect()
|
||||
throw new Error(`Invalid tab index: ${args[0]}. Open tabs: 0-${pages.length - 1}`)
|
||||
throw new Error(`No tab with id: ${requestedId}`)
|
||||
}
|
||||
|
||||
const target = pages[index]!
|
||||
const id = targetId(target)
|
||||
const url = target.url()
|
||||
await target.close()
|
||||
const page = await target.page()
|
||||
await page!.close()
|
||||
await browser.disconnect()
|
||||
console.log(`Closed tab ${index}\t${url}`)
|
||||
|
||||
console.log(`Closed tab ${id}\t${url}`)
|
||||
}
|
||||
|
||||
+3
-3
@@ -60,12 +60,12 @@ Other:
|
||||
|
||||
Tabs:
|
||||
tab list List open tabs
|
||||
tab new [url] Open new tab, print index
|
||||
tab close [n] Close tab (default: last)
|
||||
tab new [url] Open new tab, print id
|
||||
tab close [id] Close tab by id (default: last opened)
|
||||
|
||||
Flags:
|
||||
--timeout <ms> Timeout (default: 30000)
|
||||
--tab <n> Tab index (default: 0)
|
||||
--tab <id> Stable Chrome target id (default: first page)
|
||||
--json JSON output
|
||||
--right/--double Right/double click
|
||||
--fps <n> Recording frame rate (default: 30)
|
||||
|
||||
+21
-22
@@ -1,33 +1,32 @@
|
||||
import puppeteer, { type Page } from "puppeteer-core"
|
||||
import puppeteer, { type Target } from "puppeteer-core"
|
||||
import { CDP_URL, VIEWPORT } from "./config"
|
||||
|
||||
export const connect = async (tab = 0) => {
|
||||
/* puppeteer-core does not expose a public method for the underlying CDP
|
||||
target id, so we reach into `_targetId`. */
|
||||
|
||||
export const targetId = (t: Target) =>
|
||||
(t as unknown as { _targetId: string })._targetId
|
||||
|
||||
export const connect = async (tab?: string) => {
|
||||
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: CDP_URL,
|
||||
defaultViewport: VIEWPORT,
|
||||
})
|
||||
const pages = await browser.pages()
|
||||
if (tab < 0 || tab >= pages.length) {
|
||||
|
||||
const targets = browser.targets().filter((t) => t.type() === "page")
|
||||
const target = tab ? targets.find((t) => targetId(t) === tab) : targets[0]
|
||||
|
||||
const page = target ? await target.page() : null
|
||||
|
||||
if (!page) {
|
||||
await browser.disconnect()
|
||||
throw new Error(`Invalid tab index: ${tab}. Open tabs: 0-${pages.length - 1}`)
|
||||
throw new Error(tab ? `No tab with id: ${tab}` : "No open tabs")
|
||||
}
|
||||
const page = pages[tab]!
|
||||
if (!page) throw new Error("No pages found")
|
||||
return { browser, page, close: () => browser.disconnect() as Promise<void> }
|
||||
}
|
||||
|
||||
interface WithPageInput {
|
||||
tab: number
|
||||
}
|
||||
|
||||
export const withPage = async <T>(
|
||||
{ tab }: WithPageInput,
|
||||
fn: (page: Page) => Promise<T>
|
||||
) => {
|
||||
const { page, close } = await connect(tab)
|
||||
try {
|
||||
return await fn(page)
|
||||
} finally {
|
||||
await close()
|
||||
return {
|
||||
browser,
|
||||
page,
|
||||
close: () => browser.disconnect() as Promise<void>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const CDP_URL = "http://127.0.0.1:9222"
|
||||
export const VIEWPORT = { width: 1024, height: 768 }
|
||||
export const DEFAULT_TIMEOUT = 30000
|
||||
export const DEFAULT_TAB = 0
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { parseArgs } from "node:util"
|
||||
import { DEFAULT_TAB, DEFAULT_TIMEOUT } from "./config"
|
||||
import { DEFAULT_TIMEOUT } from "./config"
|
||||
|
||||
interface NumericFlagInput {
|
||||
name: string
|
||||
@@ -38,7 +38,7 @@ export const parse = (argv: string[]) => {
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
timeout: { type: "string", default: String(DEFAULT_TIMEOUT) },
|
||||
tab: { type: "string", default: String(DEFAULT_TAB) },
|
||||
tab: { type: "string" },
|
||||
json: { type: "boolean", default: false },
|
||||
right: { type: "boolean", default: false },
|
||||
double: { type: "boolean", default: false },
|
||||
@@ -58,7 +58,7 @@ export const parse = (argv: string[]) => {
|
||||
args: positionals,
|
||||
flags: {
|
||||
timeout: parseNumericFlag({ name: "timeout", value: values.timeout }),
|
||||
tab: parseNumericFlag({ name: "tab", value: values.tab }),
|
||||
tab: values.tab,
|
||||
json: values.json!,
|
||||
right: values.right!,
|
||||
double: values.double!,
|
||||
|
||||
@@ -7,7 +7,7 @@ const STATE_FILE = join(homedir(), ".mb-recorder.json")
|
||||
export interface RecorderState {
|
||||
pid: number
|
||||
path: string
|
||||
tab: number
|
||||
tab?: string
|
||||
fps: number
|
||||
scale: number
|
||||
startedAt: number
|
||||
|
||||
@@ -8,7 +8,7 @@ import { clearState, writeState, readState } from "./lib/recorder-state"
|
||||
|
||||
interface DaemonConfig {
|
||||
path: string
|
||||
tab: number
|
||||
tab?: string
|
||||
fps: number
|
||||
scale: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user