@53able/pi-agent-browser 0.1.0
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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/index.ts +523 -0
- package/package.json +52 -0
- package/skills/agent-browser/SKILL.md +62 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial Pi extension wrapping `agent-browser` CLI.
|
|
8
|
+
- Browser tools for open, read, snapshot, click, fill, screenshot, eval, close, doctor, and state management.
|
|
9
|
+
- Companion `agent-browser` skill with safe usage workflow.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tadano-go
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# pi-agent-browser
|
|
2
|
+
|
|
3
|
+
Pi extension wrapping [`vercel-labs/agent-browser`](https://github.com/vercel-labs/agent-browser) for browser automation, rendered-page interaction, screenshots, saved browser state, and agent-readable context extraction.
|
|
4
|
+
|
|
5
|
+
This package exposes `agent-browser` CLI capabilities as typed Pi tools, so Pi can use browser sessions without hand-writing shell commands.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Pi / Feynman runtime with package support
|
|
10
|
+
- [`agent-browser`](https://github.com/vercel-labs/agent-browser) installed and available on `PATH`
|
|
11
|
+
- Chrome for Testing installed through `agent-browser install`
|
|
12
|
+
|
|
13
|
+
Quick check:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
agent-browser doctor
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If `agent-browser` is not on `PATH`, set:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export AGENT_BROWSER_BIN=/absolute/path/to/agent-browser
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
From GitHub:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install https://github.com/53able/pi-agent-browser
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
From npm after publish:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pi install npm:@53able/pi-agent-browser
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
From a local checkout:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/53able/pi-agent-browser.git
|
|
43
|
+
pi install ./pi-agent-browser
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or add the package source to Pi settings manually:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"packages": ["npm:@53able/pi-agent-browser"]
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Tools
|
|
55
|
+
|
|
56
|
+
The extension registers these Pi tools:
|
|
57
|
+
|
|
58
|
+
- `agent_browser_open` — open a URL or launch `about:blank`
|
|
59
|
+
- `agent_browser_read` — extract agent-readable text or JSON from a URL or active rendered tab
|
|
60
|
+
- `agent_browser_snapshot` — get accessibility tree refs for interaction
|
|
61
|
+
- `agent_browser_click` — click by `@ref` or CSS selector
|
|
62
|
+
- `agent_browser_fill` — fill an input by `@ref` or CSS selector
|
|
63
|
+
- `agent_browser_screenshot` — capture screenshots to disk
|
|
64
|
+
- `agent_browser_eval` — evaluate JavaScript in the active tab
|
|
65
|
+
- `agent_browser_state` — save, load, list, show, rename, clear, or clean saved state
|
|
66
|
+
- `agent_browser_close` — close a browser session
|
|
67
|
+
- `agent_browser_doctor` — run `agent-browser doctor`
|
|
68
|
+
|
|
69
|
+
## Basic usage
|
|
70
|
+
|
|
71
|
+
Ask Pi naturally:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
https://example.com を browser 経由で読んで、outputs/browser/example.md に保存して
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Equivalent tool arguments for `agent_browser_read`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"url": "https://example.com",
|
|
82
|
+
"outputPath": "outputs/browser/example.md",
|
|
83
|
+
"allowedDomains": ["example.com"]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Browser interaction workflow
|
|
88
|
+
|
|
89
|
+
1. Open or navigate:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"url": "https://example.com",
|
|
94
|
+
"session": "research-example",
|
|
95
|
+
"allowedDomains": ["example.com"]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. Call `agent_browser_snapshot` to get fresh `@ref` values.
|
|
100
|
+
3. Use `agent_browser_click` or `agent_browser_fill` with those refs.
|
|
101
|
+
4. Use `agent_browser_read` for text extraction or `agent_browser_screenshot` for visual evidence.
|
|
102
|
+
5. Close the session with `agent_browser_close` when finished.
|
|
103
|
+
|
|
104
|
+
## Login/session state
|
|
105
|
+
|
|
106
|
+
To keep login state automatically, use a stable `session` and `restore: true` on `agent_browser_open`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"url": "https://example.com",
|
|
111
|
+
"session": "work-example",
|
|
112
|
+
"restore": true,
|
|
113
|
+
"headed": true
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
List saved states:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"operation": "list"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Clear one saved session state:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"operation": "clear",
|
|
130
|
+
"sessionName": "work-example"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Clear all saved states only when explicitly intended:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"operation": "clear",
|
|
139
|
+
"all": true
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Safety notes
|
|
144
|
+
|
|
145
|
+
- Prefer `allowedDomains` for task-scoped browsing.
|
|
146
|
+
- Use `outputPath` for durable source artifacts that support reports.
|
|
147
|
+
- Take a fresh snapshot after navigation, form submission, modal dismissal, or failed clicks.
|
|
148
|
+
- Prefer `agent_browser_read` for textual evidence and `agent_browser_screenshot` for visual evidence.
|
|
149
|
+
- Use `agent_browser_eval` only when `read`/`snapshot` cannot expose the required state.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
Dry-run the npm package contents:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm run pack:dry
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Local Pi test example:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
PI_OFFLINE=1 pi --no-builtin-tools --tools agent_browser_doctor -p "Use agent_browser_doctor and report output"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
168
|
+
|
|
169
|
+
## Publishing
|
|
170
|
+
|
|
171
|
+
This repository is intended to publish through npm Trusted Publishing from GitHub Actions, not through long-lived npm tokens.
|
|
172
|
+
|
|
173
|
+
### npm Trusted Publisher setup
|
|
174
|
+
|
|
175
|
+
In npm, configure a trusted publisher for the package:
|
|
176
|
+
|
|
177
|
+
- Package: `@53able/pi-agent-browser`
|
|
178
|
+
- Publisher type: GitHub Actions
|
|
179
|
+
- GitHub owner: `53able`
|
|
180
|
+
- Repository: `pi-agent-browser`
|
|
181
|
+
- Workflow filename: `publish.yml`
|
|
182
|
+
- Allowed action: `npm publish`
|
|
183
|
+
|
|
184
|
+
Then publish a release by pushing a semver tag:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git tag v0.1.0
|
|
188
|
+
git push origin v0.1.0
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The workflow uses GitHub OIDC with `id-token: write`, so no `NPM_TOKEN` secret is required.
|
package/index.ts
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import type { ExtensionAPI, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { constants, existsSync } from "node:fs";
|
|
5
|
+
import { join, resolve, basename } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
9
|
+
const DEFAULT_MAX_PREVIEW_CHARS = 12_000;
|
|
10
|
+
const DEFAULT_ARTIFACT_DIR = "outputs/browser";
|
|
11
|
+
|
|
12
|
+
type JsonObject = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
type RunResult = {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
command: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
exitCode: number | null;
|
|
21
|
+
signal: NodeJS.Signals | null;
|
|
22
|
+
durationMs: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type ToolDetails = RunResult & {
|
|
26
|
+
artifactPaths?: string[];
|
|
27
|
+
parsedJson?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function optionalString(description: string) {
|
|
31
|
+
return Type.Optional(Type.String({ description }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function optionalBoolean(description: string) {
|
|
35
|
+
return Type.Optional(Type.Boolean({ description }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function optionalStringArray(description: string) {
|
|
39
|
+
return Type.Optional(Type.Array(Type.String(), { description }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commonSchema(extra: JsonObject = {}) {
|
|
43
|
+
return {
|
|
44
|
+
session: optionalString("agent-browser session name. Use this to isolate or reuse browser state."),
|
|
45
|
+
allowedDomains: optionalStringArray("Optional domain allowlist forwarded to --allowed-domains. Example: ['example.com', '*.example.com']."),
|
|
46
|
+
maxOutput: Type.Optional(Type.Integer({ minimum: 1, description: "Forwarded to --max-output for output containment." })),
|
|
47
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Tool execution timeout in milliseconds." })),
|
|
48
|
+
extraArgs: optionalStringArray("Advanced raw agent-browser CLI args appended after generated args. Use sparingly."),
|
|
49
|
+
...extra,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const OpenSchema = Type.Object(commonSchema({
|
|
54
|
+
url: optionalString("URL to open. Omit to launch about:blank."),
|
|
55
|
+
headed: optionalBoolean("Launch headed browser window when true."),
|
|
56
|
+
profile: optionalString("Chrome profile path or profile name forwarded to --profile."),
|
|
57
|
+
restore: Type.Optional(Type.Union([Type.Boolean(), Type.String()], { description: "Enable --restore auto-save/restore, or pass a restore key/path when supported." })),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const ReadSchema = Type.Object(commonSchema({
|
|
61
|
+
url: optionalString("URL to fetch/read. Omit to read the active rendered tab DOM."),
|
|
62
|
+
json: optionalBoolean("Return agent-browser JSON output where supported."),
|
|
63
|
+
raw: optionalBoolean("Return raw response body for URL reads."),
|
|
64
|
+
outline: optionalBoolean("Return compact heading outline."),
|
|
65
|
+
requireMd: optionalBoolean("Fail unless the server returns markdown."),
|
|
66
|
+
filter: optionalString("Filter text for read extraction, outline, or llms sections."),
|
|
67
|
+
llms: Type.Optional(Type.Union([Type.Literal("index"), Type.Literal("full")], { description: "Read nearest llms.txt index or llms-full.txt." })),
|
|
68
|
+
outputPath: optionalString("Optional path to save full stdout. Relative paths resolve from cwd."),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const SnapshotSchema = Type.Object(commonSchema({
|
|
72
|
+
outputPath: optionalString("Optional path to save the snapshot stdout. Relative paths resolve from cwd."),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const ClickSchema = Type.Object(commonSchema({
|
|
76
|
+
selector: Type.String({ description: "Element ref from snapshot, such as @e2, or a CSS selector." }),
|
|
77
|
+
newTab: optionalBoolean("Open target in a new tab when supported."),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const FillSchema = Type.Object(commonSchema({
|
|
81
|
+
selector: Type.String({ description: "Input ref from snapshot, such as @e3, or a CSS selector." }),
|
|
82
|
+
text: Type.String({ description: "Text to fill into the element." }),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const ScreenshotSchema = Type.Object(commonSchema({
|
|
86
|
+
path: optionalString("Output screenshot path. Relative paths resolve from cwd. If omitted, outputs/browser is used."),
|
|
87
|
+
full: optionalBoolean("Capture full page."),
|
|
88
|
+
annotate: optionalBoolean("Annotate screenshot with numbered element labels."),
|
|
89
|
+
format: Type.Optional(Type.Union([Type.Literal("png"), Type.Literal("jpeg")], { description: "Screenshot format." })),
|
|
90
|
+
quality: Type.Optional(Type.Integer({ minimum: 0, maximum: 100, description: "JPEG quality 0-100." })),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const EvalSchema = Type.Object(commonSchema({
|
|
94
|
+
javascript: Type.String({ description: "JavaScript expression or script passed to agent-browser eval." }),
|
|
95
|
+
outputPath: optionalString("Optional path to save stdout. Relative paths resolve from cwd."),
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
const CloseSchema = Type.Object(commonSchema({
|
|
99
|
+
all: optionalBoolean("Close all active agent-browser sessions."),
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
const StateSchema = Type.Object(commonSchema({
|
|
103
|
+
operation: Type.Union([
|
|
104
|
+
Type.Literal("save"),
|
|
105
|
+
Type.Literal("load"),
|
|
106
|
+
Type.Literal("list"),
|
|
107
|
+
Type.Literal("show"),
|
|
108
|
+
Type.Literal("rename"),
|
|
109
|
+
Type.Literal("clear"),
|
|
110
|
+
Type.Literal("clean"),
|
|
111
|
+
], { description: "State operation: save/load/list/show/rename/clear/clean." }),
|
|
112
|
+
path: optionalString("Path for save/load operations."),
|
|
113
|
+
filename: optionalString("Saved state filename for show."),
|
|
114
|
+
oldName: optionalString("Old saved state name for rename."),
|
|
115
|
+
newName: optionalString("New saved state name for rename."),
|
|
116
|
+
sessionName: optionalString("Session name for clear. Distinct from global session to match agent-browser state clear [session-name]."),
|
|
117
|
+
all: optionalBoolean("Clear all saved states for operation=clear."),
|
|
118
|
+
olderThanDays: Type.Optional(Type.Integer({ minimum: 1, description: "Delete saved states older than this many days for operation=clean." })),
|
|
119
|
+
json: optionalBoolean("Forward --json for machine-readable state output."),
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
const DoctorSchema = Type.Object({
|
|
123
|
+
fix: optionalBoolean("Run agent-browser doctor --fix."),
|
|
124
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Tool execution timeout in milliseconds." })),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
async function findAgentBrowserBin(): Promise<string> {
|
|
128
|
+
if (process.env.AGENT_BROWSER_BIN) return process.env.AGENT_BROWSER_BIN;
|
|
129
|
+
const candidates = [
|
|
130
|
+
"/Users/tadano-go/.local/bin/agent-browser",
|
|
131
|
+
join(process.env.HOME ?? "", ".local/bin/agent-browser"),
|
|
132
|
+
"agent-browser",
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
for (const candidate of candidates) {
|
|
136
|
+
if (candidate === "agent-browser") return candidate;
|
|
137
|
+
try {
|
|
138
|
+
await access(candidate, constants.X_OK);
|
|
139
|
+
return candidate;
|
|
140
|
+
} catch {
|
|
141
|
+
// Try next candidate.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return "agent-browser";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pushCommonArgs(args: string[], params: JsonObject): void {
|
|
148
|
+
if (typeof params.session === "string" && params.session.trim()) {
|
|
149
|
+
args.push("--session", params.session.trim());
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(params.allowedDomains) && params.allowedDomains.length > 0) {
|
|
152
|
+
args.push("--allowed-domains", params.allowedDomains.map(String).join(","));
|
|
153
|
+
}
|
|
154
|
+
if (typeof params.maxOutput === "number" && Number.isFinite(params.maxOutput)) {
|
|
155
|
+
args.push("--max-output", String(Math.floor(params.maxOutput)));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function pushExtraArgs(args: string[], params: JsonObject): void {
|
|
160
|
+
if (Array.isArray(params.extraArgs)) {
|
|
161
|
+
for (const arg of params.extraArgs) args.push(String(arg));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runAgentBrowser(args: string[], options: { signal?: AbortSignal; timeoutMs?: number; cwd: string; onUpdate?: AgentToolUpdateCallback<ToolDetails> }): Promise<RunResult> {
|
|
166
|
+
const command = await findAgentBrowserBin();
|
|
167
|
+
const started = Date.now();
|
|
168
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
169
|
+
|
|
170
|
+
return await new Promise<RunResult>((resolvePromise) => {
|
|
171
|
+
let stdout = "";
|
|
172
|
+
let stderr = "";
|
|
173
|
+
let settled = false;
|
|
174
|
+
const child = spawn(command, args, {
|
|
175
|
+
cwd: options.cwd,
|
|
176
|
+
env: process.env,
|
|
177
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
178
|
+
signal: options.signal,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const timeout = setTimeout(() => {
|
|
182
|
+
if (!settled) {
|
|
183
|
+
stderr += `\nTimed out after ${timeoutMs}ms.`;
|
|
184
|
+
child.kill("SIGTERM");
|
|
185
|
+
}
|
|
186
|
+
}, timeoutMs);
|
|
187
|
+
|
|
188
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
189
|
+
stdout += chunk.toString("utf8");
|
|
190
|
+
});
|
|
191
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
192
|
+
stderr += chunk.toString("utf8");
|
|
193
|
+
});
|
|
194
|
+
child.on("error", (error) => {
|
|
195
|
+
if (settled) return;
|
|
196
|
+
settled = true;
|
|
197
|
+
clearTimeout(timeout);
|
|
198
|
+
resolvePromise({
|
|
199
|
+
ok: false,
|
|
200
|
+
command,
|
|
201
|
+
args,
|
|
202
|
+
stdout,
|
|
203
|
+
stderr: stderr + (stderr ? "\n" : "") + error.message,
|
|
204
|
+
exitCode: null,
|
|
205
|
+
signal: null,
|
|
206
|
+
durationMs: Date.now() - started,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
child.on("close", (exitCode, signal) => {
|
|
210
|
+
if (settled) return;
|
|
211
|
+
settled = true;
|
|
212
|
+
clearTimeout(timeout);
|
|
213
|
+
resolvePromise({
|
|
214
|
+
ok: exitCode === 0,
|
|
215
|
+
command,
|
|
216
|
+
args,
|
|
217
|
+
stdout,
|
|
218
|
+
stderr,
|
|
219
|
+
exitCode,
|
|
220
|
+
signal,
|
|
221
|
+
durationMs: Date.now() - started,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveOutputPath(ctx: ExtensionContext, requested: unknown, fallbackName: string): string {
|
|
228
|
+
if (typeof requested === "string" && requested.trim()) {
|
|
229
|
+
const p = requested.trim();
|
|
230
|
+
return p.startsWith("/") ? p : resolve(ctx.cwd, p);
|
|
231
|
+
}
|
|
232
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
233
|
+
return resolve(ctx.cwd, DEFAULT_ARTIFACT_DIR, `${stamp}-${fallbackName}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function saveArtifact(path: string, content: string): Promise<string> {
|
|
237
|
+
await mkdir(resolve(path, ".."), { recursive: true });
|
|
238
|
+
await writeFile(path, content, "utf8");
|
|
239
|
+
return path;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function preview(text: string, maxChars = DEFAULT_MAX_PREVIEW_CHARS): { text: string; truncated: boolean } {
|
|
243
|
+
if (text.length <= maxChars) return { text, truncated: false };
|
|
244
|
+
return { text: text.slice(0, maxChars) + `\n\n[truncated: ${text.length - maxChars} chars omitted]`, truncated: true };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildTextResult(result: RunResult, artifactPaths: string[] = [], parsedJson?: unknown): { content: { type: "text"; text: string }[]; details: ToolDetails } {
|
|
248
|
+
const stdoutPreview = preview(result.stdout.trim());
|
|
249
|
+
const stderrPreview = preview(result.stderr.trim(), 4000);
|
|
250
|
+
const lines = [
|
|
251
|
+
result.ok ? "agent-browser command succeeded." : "agent-browser command failed.",
|
|
252
|
+
`Command: ${result.command} ${result.args.map((a) => JSON.stringify(a)).join(" ")}`,
|
|
253
|
+
`Exit: ${result.exitCode ?? "null"}${result.signal ? ` signal=${result.signal}` : ""}`,
|
|
254
|
+
`Duration: ${result.durationMs}ms`,
|
|
255
|
+
];
|
|
256
|
+
if (artifactPaths.length > 0) {
|
|
257
|
+
lines.push("Artifacts:");
|
|
258
|
+
for (const p of artifactPaths) lines.push(`- ${p}`);
|
|
259
|
+
}
|
|
260
|
+
if (stdoutPreview.text) {
|
|
261
|
+
lines.push("Stdout:", stdoutPreview.text);
|
|
262
|
+
}
|
|
263
|
+
if (stderrPreview.text) {
|
|
264
|
+
lines.push("Stderr:", stderrPreview.text);
|
|
265
|
+
}
|
|
266
|
+
if (stdoutPreview.truncated) {
|
|
267
|
+
lines.push("Stdout preview was truncated. Read the artifact path or rerun with outputPath for full output.");
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
271
|
+
details: { ...result, artifactPaths, parsedJson },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseJsonIfRequested(result: RunResult, requested: boolean): unknown | undefined {
|
|
276
|
+
if (!requested || !result.stdout.trim()) return undefined;
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(result.stdout);
|
|
279
|
+
} catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
285
|
+
pi.registerTool({
|
|
286
|
+
name: "agent_browser_open",
|
|
287
|
+
label: "Agent Browser Open",
|
|
288
|
+
description: "Open a URL or launch an agent-browser controlled browser session. Wraps vercel-labs/agent-browser CLI.",
|
|
289
|
+
promptSnippet: "Use agent_browser_open to launch/navigate browser pages when rendered interaction is needed.",
|
|
290
|
+
promptGuidelines: [
|
|
291
|
+
"Use agent_browser_snapshot after opening or navigating before clicking by @ref.",
|
|
292
|
+
"Pass allowedDomains for untrusted or task-scoped browsing when possible.",
|
|
293
|
+
],
|
|
294
|
+
parameters: OpenSchema,
|
|
295
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
296
|
+
const params = rawParams as JsonObject;
|
|
297
|
+
const args: string[] = [];
|
|
298
|
+
pushCommonArgs(args, params);
|
|
299
|
+
if (params.headed === true) args.push("--headed");
|
|
300
|
+
if (typeof params.profile === "string" && params.profile.trim()) args.push("--profile", params.profile.trim());
|
|
301
|
+
if (params.restore === true) {
|
|
302
|
+
args.push("--restore");
|
|
303
|
+
} else if (typeof params.restore === "string" && params.restore.trim()) {
|
|
304
|
+
const restore = params.restore.trim();
|
|
305
|
+
if (restore.toLowerCase() === "true") args.push("--restore");
|
|
306
|
+
else if (restore.toLowerCase() !== "false") args.push("--restore", restore);
|
|
307
|
+
}
|
|
308
|
+
args.push("open");
|
|
309
|
+
if (typeof params.url === "string" && params.url.trim()) args.push(params.url.trim());
|
|
310
|
+
pushExtraArgs(args, params);
|
|
311
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
312
|
+
return buildTextResult(result);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
pi.registerTool({
|
|
317
|
+
name: "agent_browser_read",
|
|
318
|
+
label: "Agent Browser Read",
|
|
319
|
+
description: "Extract agent-readable text or JSON from a URL or the active rendered tab using agent-browser read.",
|
|
320
|
+
promptSnippet: "Use agent_browser_read for browser-backed or markdown-aware web context extraction. Save long outputs with outputPath.",
|
|
321
|
+
promptGuidelines: [
|
|
322
|
+
"Prefer agent_browser_read over screenshot OCR when text extraction is enough.",
|
|
323
|
+
"Use outputPath for durable source artifacts when extracted content will support a report.",
|
|
324
|
+
],
|
|
325
|
+
parameters: ReadSchema,
|
|
326
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
327
|
+
const params = rawParams as JsonObject;
|
|
328
|
+
const args: string[] = [];
|
|
329
|
+
pushCommonArgs(args, params);
|
|
330
|
+
args.push("read");
|
|
331
|
+
if (typeof params.url === "string" && params.url.trim()) args.push(params.url.trim());
|
|
332
|
+
if (params.json === true) args.push("--json");
|
|
333
|
+
if (params.raw === true) args.push("--raw");
|
|
334
|
+
if (params.outline === true) args.push("--outline");
|
|
335
|
+
if (params.requireMd === true) args.push("--require-md");
|
|
336
|
+
if (typeof params.filter === "string" && params.filter.trim()) args.push("--filter", params.filter.trim());
|
|
337
|
+
if (params.llms === "index" || params.llms === "full") args.push("--llms", params.llms);
|
|
338
|
+
pushExtraArgs(args, params);
|
|
339
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
340
|
+
const artifacts: string[] = [];
|
|
341
|
+
if (typeof params.outputPath === "string" && params.outputPath.trim()) {
|
|
342
|
+
artifacts.push(await saveArtifact(resolveOutputPath(ctx, params.outputPath, "read.txt"), result.stdout));
|
|
343
|
+
}
|
|
344
|
+
return buildTextResult(result, artifacts, parseJsonIfRequested(result, params.json === true));
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
pi.registerTool({
|
|
349
|
+
name: "agent_browser_snapshot",
|
|
350
|
+
label: "Agent Browser Snapshot",
|
|
351
|
+
description: "Get an accessibility snapshot with stable refs for browser interaction.",
|
|
352
|
+
promptSnippet: "Use agent_browser_snapshot before clicks/fills to get fresh @refs.",
|
|
353
|
+
promptGuidelines: ["Refs can go stale after navigation or DOM changes. Take a fresh snapshot before retrying failed clicks."],
|
|
354
|
+
parameters: SnapshotSchema,
|
|
355
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
356
|
+
const params = rawParams as JsonObject;
|
|
357
|
+
const args: string[] = [];
|
|
358
|
+
pushCommonArgs(args, params);
|
|
359
|
+
args.push("snapshot");
|
|
360
|
+
pushExtraArgs(args, params);
|
|
361
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
362
|
+
const artifacts: string[] = [];
|
|
363
|
+
if (typeof params.outputPath === "string" && params.outputPath.trim()) {
|
|
364
|
+
artifacts.push(await saveArtifact(resolveOutputPath(ctx, params.outputPath, "snapshot.txt"), result.stdout));
|
|
365
|
+
}
|
|
366
|
+
return buildTextResult(result, artifacts);
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
pi.registerTool({
|
|
371
|
+
name: "agent_browser_click",
|
|
372
|
+
label: "Agent Browser Click",
|
|
373
|
+
description: "Click an element by agent-browser @ref or CSS selector.",
|
|
374
|
+
promptSnippet: "Use agent_browser_click with @refs from agent_browser_snapshot or CSS selectors.",
|
|
375
|
+
promptGuidelines: ["If a click fails because an element covers the target, inspect the error, dismiss the covering element, and take a fresh snapshot."],
|
|
376
|
+
parameters: ClickSchema,
|
|
377
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
378
|
+
const params = rawParams as JsonObject;
|
|
379
|
+
const args: string[] = [];
|
|
380
|
+
pushCommonArgs(args, params);
|
|
381
|
+
args.push("click", String(params.selector));
|
|
382
|
+
if (params.newTab === true) args.push("--new-tab");
|
|
383
|
+
pushExtraArgs(args, params);
|
|
384
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
385
|
+
return buildTextResult(result);
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
pi.registerTool({
|
|
390
|
+
name: "agent_browser_fill",
|
|
391
|
+
label: "Agent Browser Fill",
|
|
392
|
+
description: "Clear and fill an input element by agent-browser @ref or CSS selector.",
|
|
393
|
+
promptSnippet: "Use agent_browser_fill for form inputs after identifying refs with agent_browser_snapshot.",
|
|
394
|
+
parameters: FillSchema,
|
|
395
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
396
|
+
const params = rawParams as JsonObject;
|
|
397
|
+
const args: string[] = [];
|
|
398
|
+
pushCommonArgs(args, params);
|
|
399
|
+
args.push("fill", String(params.selector), String(params.text));
|
|
400
|
+
pushExtraArgs(args, params);
|
|
401
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
402
|
+
return buildTextResult(result);
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
pi.registerTool({
|
|
407
|
+
name: "agent_browser_screenshot",
|
|
408
|
+
label: "Agent Browser Screenshot",
|
|
409
|
+
description: "Capture a browser screenshot and return the saved artifact path.",
|
|
410
|
+
promptSnippet: "Use agent_browser_screenshot for visual evidence, UI QA, or pages where text extraction is insufficient.",
|
|
411
|
+
parameters: ScreenshotSchema,
|
|
412
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
413
|
+
const params = rawParams as JsonObject;
|
|
414
|
+
const path = resolveOutputPath(ctx, params.path, "screenshot.png");
|
|
415
|
+
await mkdir(resolve(path, ".."), { recursive: true });
|
|
416
|
+
const args: string[] = [];
|
|
417
|
+
pushCommonArgs(args, params);
|
|
418
|
+
args.push("screenshot", path);
|
|
419
|
+
if (params.full === true) args.push("--full");
|
|
420
|
+
if (params.annotate === true) args.push("--annotate");
|
|
421
|
+
if (params.format === "png" || params.format === "jpeg") args.push("--screenshot-format", params.format);
|
|
422
|
+
if (typeof params.quality === "number" && Number.isFinite(params.quality)) args.push("--screenshot-quality", String(Math.floor(params.quality)));
|
|
423
|
+
pushExtraArgs(args, params);
|
|
424
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
425
|
+
const artifacts = existsSync(path) ? [path] : [];
|
|
426
|
+
return buildTextResult(result, artifacts);
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
pi.registerTool({
|
|
431
|
+
name: "agent_browser_eval",
|
|
432
|
+
label: "Agent Browser Eval",
|
|
433
|
+
description: "Evaluate JavaScript in the active browser tab via agent-browser eval.",
|
|
434
|
+
promptSnippet: "Use agent_browser_eval for precise DOM extraction or browser-side state checks when read/snapshot are insufficient.",
|
|
435
|
+
promptGuidelines: ["Do not use eval to bypass site security or perform destructive actions. Prefer read/snapshot for ordinary extraction."],
|
|
436
|
+
parameters: EvalSchema,
|
|
437
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
438
|
+
const params = rawParams as JsonObject;
|
|
439
|
+
const args: string[] = [];
|
|
440
|
+
pushCommonArgs(args, params);
|
|
441
|
+
args.push("eval", String(params.javascript));
|
|
442
|
+
pushExtraArgs(args, params);
|
|
443
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
444
|
+
const artifacts: string[] = [];
|
|
445
|
+
if (typeof params.outputPath === "string" && params.outputPath.trim()) {
|
|
446
|
+
artifacts.push(await saveArtifact(resolveOutputPath(ctx, params.outputPath, "eval.txt"), result.stdout));
|
|
447
|
+
}
|
|
448
|
+
return buildTextResult(result, artifacts);
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
pi.registerTool({
|
|
453
|
+
name: "agent_browser_state",
|
|
454
|
+
label: "Agent Browser State",
|
|
455
|
+
description: "Save, load, list, show, rename, clear, or clean agent-browser saved state such as cookies and web storage.",
|
|
456
|
+
promptSnippet: "Use agent_browser_state to persist or clear login/browser state when sessions and restore are involved.",
|
|
457
|
+
promptGuidelines: [
|
|
458
|
+
"Use operation=list before destructive clear/clean operations unless the user explicitly asks to clear everything.",
|
|
459
|
+
"Use session + restore on agent_browser_open for automatic state persistence; use state save/load for explicit files.",
|
|
460
|
+
"Do not clear all saved states unless the user clearly asks for global cleanup.",
|
|
461
|
+
],
|
|
462
|
+
parameters: StateSchema,
|
|
463
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
464
|
+
const params = rawParams as JsonObject;
|
|
465
|
+
const args: string[] = [];
|
|
466
|
+
pushCommonArgs(args, params);
|
|
467
|
+
if (params.json === true) args.push("--json");
|
|
468
|
+
args.push("state");
|
|
469
|
+
const operation = String(params.operation);
|
|
470
|
+
args.push(operation);
|
|
471
|
+
|
|
472
|
+
if ((operation === "save" || operation === "load") && typeof params.path === "string" && params.path.trim()) {
|
|
473
|
+
args.push(params.path.trim());
|
|
474
|
+
} else if (operation === "show" && typeof params.filename === "string" && params.filename.trim()) {
|
|
475
|
+
args.push(params.filename.trim());
|
|
476
|
+
} else if (operation === "rename" && typeof params.oldName === "string" && typeof params.newName === "string" && params.oldName.trim() && params.newName.trim()) {
|
|
477
|
+
args.push(params.oldName.trim(), params.newName.trim());
|
|
478
|
+
} else if (operation === "clear") {
|
|
479
|
+
if (typeof params.sessionName === "string" && params.sessionName.trim()) args.push(params.sessionName.trim());
|
|
480
|
+
if (params.all === true) args.push("--all");
|
|
481
|
+
} else if (operation === "clean" && typeof params.olderThanDays === "number" && Number.isFinite(params.olderThanDays)) {
|
|
482
|
+
args.push("--older-than", String(Math.floor(params.olderThanDays)));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
pushExtraArgs(args, params);
|
|
486
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
487
|
+
return buildTextResult(result, [], parseJsonIfRequested(result, params.json === true));
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
pi.registerTool({
|
|
492
|
+
name: "agent_browser_close",
|
|
493
|
+
label: "Agent Browser Close",
|
|
494
|
+
description: "Close the active agent-browser session or all sessions.",
|
|
495
|
+
promptSnippet: "Use agent_browser_close to clean up browser sessions after automation.",
|
|
496
|
+
parameters: CloseSchema,
|
|
497
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
498
|
+
const params = rawParams as JsonObject;
|
|
499
|
+
const args: string[] = [];
|
|
500
|
+
pushCommonArgs(args, params);
|
|
501
|
+
args.push("close");
|
|
502
|
+
if (params.all === true) args.push("--all");
|
|
503
|
+
pushExtraArgs(args, params);
|
|
504
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
505
|
+
return buildTextResult(result);
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
pi.registerTool({
|
|
510
|
+
name: "agent_browser_doctor",
|
|
511
|
+
label: "Agent Browser Doctor",
|
|
512
|
+
description: "Run agent-browser doctor to verify installation, Chrome, daemon, security, providers, network, and launch tests.",
|
|
513
|
+
promptSnippet: "Use agent_browser_doctor when browser automation fails or before relying on agent-browser in a workflow.",
|
|
514
|
+
parameters: DoctorSchema,
|
|
515
|
+
async execute(_id, rawParams, signal, onUpdate, ctx) {
|
|
516
|
+
const params = rawParams as JsonObject;
|
|
517
|
+
const args = ["doctor"];
|
|
518
|
+
if (params.fix === true) args.push("--fix");
|
|
519
|
+
const result = await runAgentBrowser(args, { signal, timeoutMs: params.timeoutMs as number | undefined, cwd: ctx.cwd, onUpdate });
|
|
520
|
+
return buildTextResult(result);
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@53able/pi-agent-browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension wrapping vercel-labs/agent-browser for browser automation and context extraction.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi",
|
|
10
|
+
"extension",
|
|
11
|
+
"browser",
|
|
12
|
+
"agent-browser",
|
|
13
|
+
"web-automation",
|
|
14
|
+
"context-extraction"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/53able/pi-agent-browser#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/53able/pi-agent-browser.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/53able/pi-agent-browser/issues"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"index.ts",
|
|
26
|
+
"skills",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"package.json"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"check:runtime": "node -e \"console.log('Runtime check: install this package through Pi so Pi loads index.ts with its extension loader.')\"",
|
|
34
|
+
"pack:dry": "npm pack --dry-run"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
38
|
+
"@earendil-works/pi-ai": "*",
|
|
39
|
+
"typebox": "*"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.6.0"
|
|
43
|
+
},
|
|
44
|
+
"pi": {
|
|
45
|
+
"extensions": [
|
|
46
|
+
"./index.ts"
|
|
47
|
+
],
|
|
48
|
+
"skills": [
|
|
49
|
+
"./skills"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-browser
|
|
3
|
+
description: Use vercel-labs/agent-browser from Pi for browser automation, rendered-page interaction, screenshots, and agent-readable web context extraction. Use when a task requires interacting with a website, reading authenticated/rendered DOM state, capturing visual evidence, or extracting page context beyond simple HTTP fetch.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent Browser for Pi
|
|
7
|
+
|
|
8
|
+
Use the `agent_browser_*` tools instead of raw `bash` commands when browser state, screenshots, or structured artifacts matter.
|
|
9
|
+
|
|
10
|
+
## Core workflow
|
|
11
|
+
|
|
12
|
+
1. Open or navigate with `agent_browser_open`.
|
|
13
|
+
2. Extract text with `agent_browser_read` when text is enough.
|
|
14
|
+
3. Use `agent_browser_snapshot` before interactive actions to get fresh `@ref` identifiers.
|
|
15
|
+
4. Use `agent_browser_click` and `agent_browser_fill` with `@ref` values from the latest snapshot.
|
|
16
|
+
5. Capture evidence with `agent_browser_screenshot` when layout or visual state matters.
|
|
17
|
+
6. Persist or clear login/browser state with `agent_browser_state` when needed.
|
|
18
|
+
7. Close sessions with `agent_browser_close` when finished.
|
|
19
|
+
|
|
20
|
+
## Safety defaults
|
|
21
|
+
|
|
22
|
+
- Pass `allowedDomains` whenever the browsing task has a known scope.
|
|
23
|
+
- Use `outputPath` for durable source artifacts that support reports or later verification.
|
|
24
|
+
- Take a fresh snapshot after navigation, form submission, modal dismissal, or any failed click.
|
|
25
|
+
- Prefer `agent_browser_read` over screenshots for textual evidence.
|
|
26
|
+
- Use `agent_browser_eval` only when read/snapshot cannot expose the required state.
|
|
27
|
+
- To keep login state automatically, open with a stable `session` and `restore: true`.
|
|
28
|
+
- Before clearing state, list saved states with `agent_browser_state` unless the user explicitly asks for broad cleanup.
|
|
29
|
+
|
|
30
|
+
## Common examples
|
|
31
|
+
|
|
32
|
+
Read a public page:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{"url":"https://example.com","outputPath":"outputs/browser/example.md"}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Open, snapshot, click:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{"url":"https://example.com","session":"research-example","allowedDomains":["example.com"]}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then call `agent_browser_snapshot`, inspect refs, and call `agent_browser_click` with the chosen `@ref`.
|
|
45
|
+
|
|
46
|
+
Maintain login state automatically:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{"url":"https://example.com","session":"work-example","restore":true}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
List saved states:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"operation":"list"}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Clear a specific saved session state:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{"operation":"clear","sessionName":"work-example"}
|
|
62
|
+
```
|