pi-browser-harness 0.11.0 → 0.12.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 +26 -0
- package/README.md +16 -1
- package/package.json +2 -2
- package/skills/pi-browser-harness/SKILL.md +29 -0
- package/src/cdp/commands.ts +4 -0
- package/src/cdp/discovery.ts +15 -12
- package/src/daemon/bridge.ts +262 -99
- package/src/daemon/spawn.ts +72 -14
- package/src/daemon/transport.ts +7 -2
- package/src/domains/click.ts +0 -5
- package/src/domains/console.ts +0 -2
- package/src/domains/files.ts +53 -4
- package/src/domains/form-batch.ts +0 -1
- package/src/domains/form.ts +1 -9
- package/src/domains/headers.ts +89 -0
- package/src/domains/history.ts +3 -3
- package/src/domains/js.ts +3 -9
- package/src/domains/keyboard.ts +2 -7
- package/src/domains/navigate.ts +0 -3
- package/src/domains/network.ts +1 -3
- package/src/domains/page.ts +1 -7
- package/src/domains/readpage/read-page.ts +0 -1
- package/src/domains/screenshot.ts +0 -4
- package/src/domains/scroll.ts +0 -1
- package/src/domains/search/web-search.ts +0 -1
- package/src/domains/setup.ts +0 -2
- package/src/domains/snapshot.ts +1 -4
- package/src/domains/tabs.ts +1 -9
- package/src/domains/viewport.ts +0 -1
- package/src/index.ts +16 -12
- package/src/prompt.ts +22 -122
- package/src/registry.ts +3 -0
- package/src/state.ts +32 -2
- package/src/util/paths.ts +11 -1
package/src/prompt.ts
CHANGED
|
@@ -1,127 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* This prompt carries only cross-cutting knowledge — the mental model, tool
|
|
6
|
-
* routing, workflows, and gotchas. Per-tool specifics (schemas, detailed usage)
|
|
7
|
-
* are injected separately via promptGuidelines on each tool registration, so this
|
|
8
|
-
* file intentionally does NOT restate full tool descriptions.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export function getBrowserSystemPrompt(): string {
|
|
1
|
+
export function getBrowserSystemPrompt(connected = true): string {
|
|
2
|
+
const connectionNotice = connected
|
|
3
|
+
? ""
|
|
4
|
+
: "Browser is not connected. Run browser_setup or /browser-setup to connect.\n\n";
|
|
12
5
|
return `
|
|
13
6
|
## Browser Control
|
|
14
7
|
|
|
15
|
-
You
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Choosing a tool
|
|
35
|
-
|
|
36
|
-
| To… | Reach for |
|
|
37
|
-
|-----|-----------|
|
|
38
|
-
| See the page structure (for interaction) | \`browser_snapshot\` (refs) |
|
|
39
|
-
| See the page visually | \`browser_screenshot\` |
|
|
40
|
-
| Search the web | \`browser_web_search\` → ranked {title, url, snippet} |
|
|
41
|
-
| Read an article's clean text | \`browser_read_page\` (reader mode; url or owned tab) |
|
|
42
|
-
| Extract specific DOM values | \`browser_execute_js\` |
|
|
43
|
-
| Hit a JSON/API endpoint | \`browser_http_get\` (raw GET, outside the browser) |
|
|
44
|
-
| Click / fill / type / select / focus / press a key | \`browser_click\` · \`browser_fill\` · \`browser_type\` · \`browser_select_option\` · \`browser_focus\` · \`browser_press_key\` |
|
|
45
|
-
| Upload a file · drag · resize viewport | \`browser_upload_file\` · \`browser_drag_and_drop\` · \`browser_viewport_resize\` |
|
|
46
|
-
| Go to a URL / open many / manage tabs | \`browser_navigate\` · \`browser_new_tab\` · \`browser_open_urls\` · \`browser_list_tabs\` · \`browser_switch_tab\` · \`browser_go_back\`/\`go_forward\`/\`reload\` |
|
|
47
|
-
| Wait for content / load / a fixed delay | \`browser_wait_for\` (selector/text) · \`browser_wait_for_load\` (readyState) · \`browser_wait\` |
|
|
48
|
-
| Inspect page / handle a dialog | \`browser_page_info\` · \`browser_current_tab\` · \`browser_handle_dialog\` |
|
|
49
|
-
| Diagnose a broken action | \`browser_console\` (JS errors) · \`browser_get_network_log\` |
|
|
50
|
-
| Save output / config downloads | \`browser_print_to_pdf\` · \`browser_download\` |
|
|
51
|
-
| Anything the tools can't express | \`browser_run_script\` (see Extending) |
|
|
52
|
-
|
|
53
|
-
Each tool carries its own detailed guidelines — this table is only for routing.
|
|
54
|
-
|
|
55
|
-
### Workflows
|
|
56
|
-
|
|
57
|
-
**Research (search → read):**
|
|
58
|
-
\`\`\`
|
|
59
|
-
browser_web_search("query") // ranked links, no page content
|
|
60
|
-
→ browser_read_page(url) // clean article text for the top hits
|
|
61
|
-
// fall back to browser_open_urls + browser_execute_js only when reader mode misses
|
|
62
|
-
\`\`\`
|
|
63
|
-
|
|
64
|
-
**Form filling (ref-first — the reliable path on SPA forms):**
|
|
65
|
-
\`\`\`
|
|
66
|
-
browser_wait_for({ selector: "#email" }) // ensure the field is rendered
|
|
67
|
-
browser_snapshot() // interactive elements get [eN] refs
|
|
68
|
-
→ browser_fill({ ref: "e7", value: "a@b.com" })
|
|
69
|
-
→ browser_select_option({ ref: "e9", label: "India" })
|
|
70
|
-
→ browser_click({ ref: "e12" }) // re-resolves position even after reflow
|
|
71
|
-
// read each "Page changes" diff to confirm the step landed
|
|
72
|
-
\`\`\`
|
|
73
|
-
|
|
74
|
-
**Navigate + verify:**
|
|
75
|
-
\`\`\`
|
|
76
|
-
browser_new_tab("https://example.com") → browser_wait_for_load() → browser_screenshot()
|
|
77
|
-
\`\`\`
|
|
78
|
-
|
|
79
|
-
**Scroll:** \`browser_scroll({ deltaY: 500 })\` — W3C wheel convention: positive = down,
|
|
80
|
-
negative = up (default 300, down).
|
|
81
|
-
|
|
82
|
-
### Parallelism
|
|
83
|
-
|
|
84
|
-
Observation tools (\`browser_screenshot\`, \`browser_page_info\`, \`browser_execute_js\`,
|
|
85
|
-
\`browser_list_tabs\`, \`browser_http_get\`, \`browser_web_search\`, \`browser_read_page\`, …)
|
|
86
|
-
run in parallel with each other and with mutations. The harness serializes mutations
|
|
87
|
-
(click, type, scroll, navigate, switch_tab) so they never race. Emit independent calls
|
|
88
|
-
in one turn:
|
|
89
|
-
\`\`\`
|
|
90
|
-
browser_screenshot() + browser_page_info() + browser_execute_js("document.title")
|
|
91
|
-
\`\`\`
|
|
92
|
-
|
|
93
|
-
**Multi-agent:** tab switching by one agent changes the active tab for all agents,
|
|
94
|
-
but per-tab data (console, network, dialogs) stays isolated. Call \`browser_current_tab\`
|
|
95
|
-
before a mutation to confirm you're on the expected tab. Handle any dialog reported by
|
|
96
|
-
\`browser_page_info\` promptly — dialogs block interaction and aren't queued across agents.
|
|
97
|
-
|
|
98
|
-
**Session boundary & tab hygiene:** you operate only inside this session's own Chrome
|
|
99
|
-
window. \`browser_switch_tab\`/\`browser_close_tab\` refuse tabs this session didn't open,
|
|
100
|
-
and \`browser_list_tabs\` defaults to owned tabs — never reach into the user's other tabs
|
|
101
|
-
or windows. Close tabs when you're done with them: call \`browser_close_tab\` on any tab
|
|
102
|
-
you opened (via \`browser_new_tab\`/\`browser_open_urls\`) once you've extracted what you
|
|
103
|
-
need. Don't leave a pile of stale tabs behind — keep only what a later step still needs.
|
|
104
|
-
(Leftover tabs are closed automatically when the session ends, but close them yourself
|
|
105
|
-
as you go.)
|
|
106
|
-
|
|
107
|
-
### Extending: temporary scripts
|
|
108
|
-
|
|
109
|
-
When built-in tools can't express a multi-step workflow, write a script to a temp file
|
|
110
|
-
and run it with \`browser_run_script\`. It executes in the harness process with direct
|
|
111
|
-
daemon + Node access.
|
|
112
|
-
\`\`\`
|
|
113
|
-
write(<tempfile>.js, \`
|
|
114
|
-
const results = [];
|
|
115
|
-
for (const url of params.urls) {
|
|
116
|
-
await daemon.cdp("Page.navigate", { url });
|
|
117
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
118
|
-
results.push({ url, title: await daemon.evaluateJS("document.title") });
|
|
119
|
-
}
|
|
120
|
-
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
121
|
-
\`)
|
|
122
|
-
browser_run_script(<tempfile>.js, { urls: [...] })
|
|
123
|
-
\`\`\`
|
|
124
|
-
Script bindings: params, daemon, require, signal, onUpdate, ctx, console, fetch, JSON,
|
|
125
|
-
Buffer, setTimeout, clearTimeout. Use a real temp path from the OS — don't hardcode \`/tmp\`.
|
|
8
|
+
${connectionNotice}You control the user's running Chrome through \`browser_*\` tools alongside the
|
|
9
|
+
standard pi tools. Per-tool usage lives in tool descriptions, parameter schemas,
|
|
10
|
+
and tool-specific guidelines.
|
|
11
|
+
|
|
12
|
+
- Operate only inside this session's owned Chrome window. Never reach into the
|
|
13
|
+
user's other tabs or windows. Close owned tabs once no later step needs them.
|
|
14
|
+
- Mutating tools (click, type, fill, press_key, scroll, navigate, history, tab
|
|
15
|
+
switch/new/close, upload, download, headers, viewport, drag, web_search) are
|
|
16
|
+
serialized. Observations (snapshot, screenshot, page_info, current_tab, list_tabs,
|
|
17
|
+
console, network_requests, http_get, read_page, execute_js, wait, wait_for) may run
|
|
18
|
+
concurrently with them. \`browser_run_script\` bypasses that lane: keep mutating
|
|
19
|
+
scripts sequential with other browser calls. Parallelize only independent calls.
|
|
20
|
+
- Agents sharing a session share its active tab. Check \`browser_current_tab\`
|
|
21
|
+
before a mutation to confirm the expected tab when another agent may switch it.
|
|
22
|
+
- Read any appended "Page changes" diff to confirm an interaction. Tools without a
|
|
23
|
+
diff, including \`browser_run_script\`, need explicit state verification.
|
|
24
|
+
- If a tool reports "ref is stale", call \`browser_snapshot\` for fresh refs before
|
|
25
|
+
retrying; do not reuse the stale handle.
|
|
126
26
|
`;
|
|
127
27
|
}
|
package/src/registry.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { uploadFileTool, downloadTool, printToPdfTool } from "./domains/files";
|
|
|
16
16
|
import { viewportResizeTool } from "./domains/viewport";
|
|
17
17
|
import { dragAndDropTool } from "./domains/drag";
|
|
18
18
|
import { httpGetTool, networkRequestsTool } from "./domains/network";
|
|
19
|
+
import { clearHeadersTool, setHeadersTool } from "./domains/headers";
|
|
19
20
|
import { consoleTool } from "./domains/console";
|
|
20
21
|
import { snapshotTool } from "./domains/snapshot";
|
|
21
22
|
import { executeJsTool, runScriptTool } from "./domains/js";
|
|
@@ -58,6 +59,8 @@ export const ALL_TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
|
|
|
58
59
|
dragAndDropTool,
|
|
59
60
|
httpGetTool,
|
|
60
61
|
networkRequestsTool,
|
|
62
|
+
setHeadersTool,
|
|
63
|
+
clearHeadersTool,
|
|
61
64
|
consoleTool,
|
|
62
65
|
snapshotTool,
|
|
63
66
|
executeJsTool,
|
package/src/state.ts
CHANGED
|
@@ -13,8 +13,38 @@ export const defaultState = (namespace = "default"): BrowserState => ({
|
|
|
13
13
|
ownedTargetIds: [],
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Pi session JSONL is append-only and every custom entry shows up in the branch navigator, so a
|
|
17
|
+
// snapshot is only worth appending when it differs from the one the branch already holds (#26).
|
|
18
|
+
// Tab order is not a difference: ownership is a set.
|
|
19
|
+
const stateKey = (state: BrowserState): string =>
|
|
20
|
+
JSON.stringify({
|
|
21
|
+
namespace: state.namespace,
|
|
22
|
+
ownedTargetIds: [...(state.ownedTargetIds ?? [])].sort(),
|
|
23
|
+
harnessWindowTargetId: state.harnessWindowTargetId ?? null,
|
|
24
|
+
harnessWindowId: state.harnessWindowId ?? null,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type StatePersister = {
|
|
28
|
+
// Appends the snapshot unless it equals the last one persisted or restored. Returns whether it wrote.
|
|
29
|
+
readonly persist: (state: BrowserState) => boolean;
|
|
30
|
+
// Records a snapshot that already lives in the session (restored from the branch) without writing it.
|
|
31
|
+
readonly markPersisted: (state: BrowserState) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const createStatePersister = (pi: ExtensionAPI): StatePersister => {
|
|
35
|
+
let last: string | undefined;
|
|
36
|
+
return {
|
|
37
|
+
persist: (state) => {
|
|
38
|
+
const key = stateKey(state);
|
|
39
|
+
if (key === last) return false;
|
|
40
|
+
pi.appendEntry<BrowserState>("browser-harness-state", state);
|
|
41
|
+
last = key;
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
markPersisted: (state) => {
|
|
45
|
+
last = stateKey(state);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
18
48
|
};
|
|
19
49
|
|
|
20
50
|
const asPersistedState = (v: unknown): Partial<BrowserState> | undefined => {
|
package/src/util/paths.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
// `relative` rather than a string prefix: it is separator-agnostic and carries each platform's
|
|
6
|
+
// own case rules, so a Windows path is not rejected for its backslashes or its drive-letter case.
|
|
7
|
+
export const isPathWithin = (root: string, target: string): boolean => {
|
|
8
|
+
const absRoot = resolve(root);
|
|
9
|
+
const absTarget = resolve(target);
|
|
10
|
+
if (absTarget === absRoot) return true;
|
|
11
|
+
const rel = relative(absRoot, absTarget);
|
|
12
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
13
|
+
};
|
|
4
14
|
|
|
5
15
|
const NAMESPACE_RE = /^[a-zA-Z0-9_-]+$/;
|
|
6
16
|
|