@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 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
+ ```