pi-browser-harness 0.4.0 → 0.6.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 +27 -0
- package/README.md +150 -56
- package/package.json +2 -1
- package/skills/pi-browser-harness/SKILL.md +24 -1
- package/src/cdp/console-buffer.ts +263 -0
- package/src/cdp/daemon-transport.ts +277 -0
- package/src/cdp/session.ts +124 -23
- package/src/client.ts +29 -10
- package/src/daemon/bridge.ts +411 -0
- package/src/daemon/index.ts +114 -0
- package/src/daemon/protocol.ts +162 -0
- package/src/daemon/server.ts +245 -0
- package/src/daemon/spawn.ts +85 -0
- package/src/domains/console.ts +173 -0
- package/src/domains/navigate.ts +17 -3
- package/src/domains/setup.ts +41 -0
- package/src/index.ts +46 -25
- package/src/prompt.ts +3 -0
- package/src/registry.ts +5 -0
- package/src/setup.ts +45 -39
- package/src/util/tool.ts +19 -0
package/src/prompt.ts
CHANGED
|
@@ -36,6 +36,7 @@ to interact with pages visually, and other tools alongside them.
|
|
|
36
36
|
| browser_switch_tab | Switch to a different tab by targetId |
|
|
37
37
|
| browser_execute_js | Execute JavaScript and return the result |
|
|
38
38
|
| browser_http_get | Direct HTTP GET (outside browser, for APIs) |
|
|
39
|
+
| browser_console | Read JS errors / console output on the current tab (diagnostic — use when an action looks broken; pass sinceSeq from the previous nextCursor to see only new messages) |
|
|
39
40
|
| browser_wait | Wait N seconds |
|
|
40
41
|
| browser_wait_for_load | Wait for document.readyState === 'complete' (returns a typed timeout error if the page doesn't reach readyState=complete in N seconds, default 15) |
|
|
41
42
|
| browser_handle_dialog | Accept or dismiss a JS dialog |
|
|
@@ -73,6 +74,8 @@ browser_screenshot() + browser_page_info() + browser_execute_js("document.title"
|
|
|
73
74
|
browser_http_get("https://api.example.com/data") + browser_click(x, y)
|
|
74
75
|
\`\`\`
|
|
75
76
|
|
|
77
|
+
**Multi-agent note**: When multiple subagents use the browser, tab switching by one agent changes the active tab for all agents. Per-tab data (console buffers, network traces, dialogs, page info cache) is isolated — switching tabs does not destroy another agent's collected data. Call \`browser_current_tab\` before mutation tools to confirm you're on the expected tab. If \`browser_page_info\` returns a dialog, handle it with \`browser_handle_dialog\` promptly — dialogs block page interaction and are not queued across agents.
|
|
78
|
+
|
|
76
79
|
### Common Patterns
|
|
77
80
|
|
|
78
81
|
**Navigation:**
|
package/src/registry.ts
CHANGED
|
@@ -14,13 +14,17 @@ import { uploadFileTool, downloadTool, printToPdfTool } from "./domains/files";
|
|
|
14
14
|
import { viewportResizeTool } from "./domains/viewport";
|
|
15
15
|
import { dragAndDropTool } from "./domains/drag";
|
|
16
16
|
import { httpGetTool, networkRequestsTool } from "./domains/network";
|
|
17
|
+
import { consoleTool } from "./domains/console";
|
|
17
18
|
import { snapshotTool } from "./domains/snapshot";
|
|
18
19
|
import { executeJsTool, runScriptTool } from "./domains/js";
|
|
20
|
+
import { setupTool } from "./domains/setup";
|
|
19
21
|
|
|
20
22
|
// Mutation tools that modify page / browser state are marked `serialized: true`
|
|
21
23
|
// so they run through a shared async mutex. Observation / read-only tools are
|
|
22
24
|
// left unmarked and can execute in parallel.
|
|
23
25
|
const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
|
|
26
|
+
// setup comes first — the agent calls this to self-recover from not-initialized
|
|
27
|
+
setupTool,
|
|
24
28
|
{ ...clickTool, serialized: true },
|
|
25
29
|
{ ...typeTool, serialized: true },
|
|
26
30
|
{ ...pressKeyTool, serialized: true },
|
|
@@ -48,6 +52,7 @@ const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
|
|
|
48
52
|
{ ...dragAndDropTool, serialized: true },
|
|
49
53
|
httpGetTool,
|
|
50
54
|
networkRequestsTool,
|
|
55
|
+
consoleTool,
|
|
51
56
|
snapshotTool,
|
|
52
57
|
executeJsTool,
|
|
53
58
|
runScriptTool,
|
package/src/setup.ts
CHANGED
|
@@ -1,37 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Setup
|
|
3
|
-
*
|
|
2
|
+
* Setup — verifies Chrome is running with remote debugging and connects
|
|
3
|
+
* the pi agent to it. Exposed as both a slash command (/browser-setup) for
|
|
4
|
+
* users and a tool (browser_setup) for the agent to self-recover.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import type { ExtensionAPI
|
|
7
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
8
|
import { execSync } from "node:child_process";
|
|
8
9
|
import type { BrowserClient } from "./client";
|
|
10
|
+
import { ensureDaemon } from "./daemon/spawn";
|
|
11
|
+
|
|
12
|
+
// ── Public: register the /browser-setup slash command ──────────────────────
|
|
9
13
|
|
|
10
14
|
export function registerSetupCommand(pi: ExtensionAPI, client: BrowserClient): void {
|
|
11
15
|
pi.registerCommand("browser-setup", {
|
|
12
16
|
description: "Connect pi to your Chrome browser",
|
|
13
17
|
handler: async (_args, ctx) => {
|
|
14
|
-
await
|
|
18
|
+
const result = await performSetup(client);
|
|
19
|
+
if (result.success) {
|
|
20
|
+
ctx.ui.notify(result.data, "info");
|
|
21
|
+
} else {
|
|
22
|
+
ctx.ui.notify(result.error, "error");
|
|
23
|
+
}
|
|
15
24
|
},
|
|
16
25
|
});
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
// ── Shared setup result ────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export type SetupResult = { success: true; data: string } | { success: false; error: string };
|
|
31
|
+
|
|
32
|
+
// ── Shared setup logic (used by both the command and the tool) ─────────────
|
|
21
33
|
|
|
34
|
+
export async function performSetup(client: BrowserClient): Promise<SetupResult> {
|
|
22
35
|
// Step 1: Check Chrome is running
|
|
23
36
|
const chromeRunning = checkChromeRunning();
|
|
24
37
|
if (!chromeRunning) {
|
|
25
|
-
|
|
26
|
-
"No browser instance running. Please open your browser and then run /browser-setup.",
|
|
27
|
-
"error",
|
|
28
|
-
);
|
|
29
|
-
return;
|
|
38
|
+
return { success: false, error: "No browser instance running. Please open your browser and then run /browser-setup." };
|
|
30
39
|
}
|
|
31
|
-
ctx.ui.notify("Chrome is running ✓", "info");
|
|
32
40
|
|
|
33
|
-
// Step 2:
|
|
34
|
-
|
|
41
|
+
// Step 2: Start the browser daemon (spawns if not running, silently reuses if alive)
|
|
42
|
+
const daemonReady = await ensureDaemon();
|
|
43
|
+
if (!daemonReady) {
|
|
44
|
+
return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 3: Connect to Chrome DevTools
|
|
35
48
|
const startResult = await client.start();
|
|
36
49
|
if (!startResult.success) {
|
|
37
50
|
const msg = startResult.error.message;
|
|
@@ -43,30 +56,23 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v
|
|
|
43
56
|
lower.includes("econnrefused") ||
|
|
44
57
|
lower.includes("cannot reach chrome devtools")
|
|
45
58
|
) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error:
|
|
62
|
+
"Chrome remote debugging needs to be enabled.\n\n" +
|
|
63
|
+
'Open chrome://inspect/#remote-debugging in your browser, tick the\n' +
|
|
64
|
+
'"Discover network targets" / Allow checkbox, then retry.\n\n' +
|
|
50
65
|
"Or set BU_CDP_WS to a remote browser WebSocket URL.",
|
|
51
|
-
|
|
52
|
-
);
|
|
53
|
-
return;
|
|
66
|
+
};
|
|
54
67
|
}
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
return;
|
|
69
|
+
return { success: false, error: `Connection failed: ${msg}` };
|
|
58
70
|
}
|
|
59
|
-
ctx.ui.notify("Connected to Chrome ✓", "info");
|
|
60
71
|
|
|
61
|
-
// Step
|
|
62
|
-
ctx.ui.notify("Testing browser control...", "info");
|
|
72
|
+
// Step 4: Verify with test navigation
|
|
63
73
|
const tabResult = await client.newTab("https://github.com");
|
|
64
74
|
if (!tabResult.success) {
|
|
65
|
-
|
|
66
|
-
`Browser connected but test navigation failed: ${tabResult.error.message}`,
|
|
67
|
-
"warning",
|
|
68
|
-
);
|
|
69
|
-
return;
|
|
75
|
+
return { success: false, error: `Browser connected but test navigation failed: ${tabResult.error.message}` };
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
const info = await client.pageInfo();
|
|
@@ -75,24 +81,24 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v
|
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
const pageUrl = info.success && !("dialog" in info.data) ? info.data.url : "github.com";
|
|
78
|
-
|
|
84
|
+
return { success: true, data: `Browser connected ✓\nNavigated to: ${pageUrl}` };
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
// ── Chrome process detection ───────────────────────────────────────────────
|
|
88
|
+
|
|
81
89
|
function checkChromeRunning(): boolean {
|
|
82
90
|
try {
|
|
83
91
|
if (process.platform === "darwin") {
|
|
84
|
-
//
|
|
85
|
-
//
|
|
92
|
+
// ps -o comm= returns the full executable path on modern macOS.
|
|
93
|
+
// We check for "Google Chrome" anywhere in the path (case-insensitive),
|
|
94
|
+
// excluding helper/renderer subprocesses.
|
|
86
95
|
const out = execSync("ps -A -o comm=", { timeout: 5000 }).toString().toLowerCase();
|
|
87
96
|
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
88
|
-
const browserProcesses = ["google chrome", "chromium", "microsoft edge"];
|
|
89
97
|
return lines.some((l) => {
|
|
90
|
-
if (l.includes("helper") || l.includes("renderer")) return false;
|
|
91
|
-
return
|
|
98
|
+
if (l.includes("helper") || l.includes("renderer") || l.includes("crashpad")) return false;
|
|
99
|
+
return l.includes("google chrome") || l.includes("chromium") || l.includes("microsoft edge");
|
|
92
100
|
});
|
|
93
101
|
} else if (process.platform === "linux") {
|
|
94
|
-
// Use args to distinguish the main browser process from sub-processes
|
|
95
|
-
// (GPU, renderer, utility, etc.) which carry --type= flags.
|
|
96
102
|
const out = execSync("ps -A -o comm=,args=", { timeout: 5000 }).toString().toLowerCase();
|
|
97
103
|
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
98
104
|
const browserComms = ["chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge", "google-chrome"];
|
package/src/util/tool.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
ToolRenderResultOptions,
|
|
10
10
|
} from "@mariozechner/pi-coding-agent";
|
|
11
11
|
import type { Component } from "@mariozechner/pi-tui";
|
|
12
|
+
import { access } from "node:fs/promises";
|
|
13
|
+
import { DAEMON_SOCKET_PATH } from "../daemon/protocol";
|
|
12
14
|
import type { BrowserClient } from "../client";
|
|
13
15
|
import type { Result } from "./result";
|
|
14
16
|
|
|
@@ -142,6 +144,23 @@ export const registerBrowserTool = (
|
|
|
142
144
|
: {}),
|
|
143
145
|
async execute(_toolCallId, args, signal, onUpdate, extensionCtx) {
|
|
144
146
|
if (def.ensureAlive !== false) {
|
|
147
|
+
// Check daemon socket exists before attempting browser control.
|
|
148
|
+
// If missing, the user hasn't run /browser-setup yet. Return a clear
|
|
149
|
+
// error so the agent knows to direct the user to initialize.
|
|
150
|
+
try {
|
|
151
|
+
await access(DAEMON_SOCKET_PATH);
|
|
152
|
+
} catch {
|
|
153
|
+
return toToolResult(
|
|
154
|
+
{
|
|
155
|
+
success: false,
|
|
156
|
+
error: {
|
|
157
|
+
kind: "not_connected",
|
|
158
|
+
message: "Browser harness not initialized. Run /browser-setup first",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
def.name,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
145
164
|
const alive = await client.ensureAlive();
|
|
146
165
|
if (!alive.success) {
|
|
147
166
|
return toToolResult(
|