pi-browser-use 0.10.1 → 0.11.1
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/dist/annotate.d.ts +0 -14
- package/dist/annotate.js +0 -14
- package/dist/artifacts.d.ts +0 -2
- package/dist/artifacts.js +0 -2
- package/dist/auth-verifiers.d.ts +0 -29
- package/dist/auth-verifiers.js +0 -31
- package/dist/chrome-launcher.d.ts +0 -59
- package/dist/chrome-launcher.js +1 -55
- package/dist/client.d.ts +0 -4
- package/dist/client.js +0 -17
- package/dist/config.d.ts +0 -25
- package/dist/config.js +0 -23
- package/dist/doctor.d.ts +0 -8
- package/dist/doctor.js +0 -5
- package/dist/existing-flow.d.ts +0 -31
- package/dist/existing-flow.js +0 -29
- package/dist/focus-policy.d.ts +0 -13
- package/dist/focus-policy.js +0 -13
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +0 -10
- package/dist/named-profile.d.ts +0 -36
- package/dist/named-profile.js +0 -37
- package/dist/persistent-backend.d.ts +2 -56
- package/dist/persistent-backend.js +30 -65
- package/dist/persistent-store.d.ts +0 -24
- package/dist/persistent-store.js +0 -24
- package/dist/profile-lock.d.ts +0 -21
- package/dist/profile-lock.js +0 -27
- package/dist/profile.d.ts +0 -10
- package/dist/profile.js +0 -14
- package/dist/runtime.d.ts +0 -3
- package/dist/runtime.js +3 -92
- package/dist/session-manager.d.ts +0 -48
- package/dist/session-manager.js +0 -46
- package/dist/session.d.ts +0 -42
- package/dist/session.js +0 -18
- package/dist/settings.d.ts +0 -5
- package/dist/settings.js +0 -5
- package/dist/setup-flow.d.ts +0 -41
- package/dist/setup-flow.js +0 -37
- package/dist/shared-backend.d.ts +0 -30
- package/dist/shared-backend.js +0 -31
- package/dist/tab-bridge.d.ts +0 -27
- package/dist/tab-bridge.js +0 -27
- package/dist/tool-augment.d.ts +0 -9
- package/dist/tool-augment.js +0 -14
- package/dist/vision.d.ts +0 -17
- package/dist/vision.js +0 -17
- package/docs/performance.md +14 -0
- package/extension/README.md +2 -0
- package/extension/background.js +4 -9
- package/package.json +4 -2
- package/plugin.json +1 -1
package/dist/annotate.d.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Numbered-badge annotation for screenshots (adapted from the technique
|
|
3
|
-
* popularized by vercel-labs/agent-browser).
|
|
4
|
-
*
|
|
5
|
-
* Badges are injected around visible interactive elements, screenshotted,
|
|
6
|
-
* then removed. The returned coordinate map pairs each badge number with a
|
|
7
|
-
* viewport center point for coordinate click tools, plus a short label so
|
|
8
|
-
* vision models can resolve "click the blue submit button" to (x, y).
|
|
9
|
-
*/
|
|
10
1
|
export interface AnnotatedElement {
|
|
11
2
|
n: number;
|
|
12
3
|
x: number;
|
|
@@ -17,11 +8,6 @@ export interface AnnotatedElement {
|
|
|
17
8
|
export declare const ANNOTATE_MARKER = "data-pi-annotate";
|
|
18
9
|
export declare const INJECT_ANNOTATIONS = "() => {\n document.querySelectorAll('[data-pi-annotate]').forEach((el) => el.remove());\n const els = [...document.querySelectorAll('a, button, input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"checkbox\"], [role=\"switch\"]')].filter((el) => {\n const r = el.getBoundingClientRect();\n return r.width > 0 && r.height > 0 && r.top >= 0 && r.left >= 0 && r.top < window.innerHeight && r.left < window.innerWidth;\n }).slice(0, 50);\n return els.map((el, i) => {\n const r = el.getBoundingClientRect();\n const badge = document.createElement('div');\n badge.setAttribute('data-pi-annotate', '1');\n badge.textContent = String(i + 1);\n badge.style.cssText = 'position:fixed;left:' + r.left + 'px;top:' + r.top + 'px;z-index:2147483647;background:#7c3aed;color:#fff;font:12px/1.4 monospace;padding:1px 5px;border-radius:3px;pointer-events:none;';\n document.body.appendChild(badge);\n const label = el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '';\n return { n: i + 1, x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2), tag: el.tagName.toLowerCase(), text: String(label).replace(/\\s+/g, ' ').slice(0, 60) };\n });\n}";
|
|
19
10
|
export declare const CLEANUP_ANNOTATIONS = "() => {\n const badges = document.querySelectorAll('[data-pi-annotate]');\n const count = badges.length;\n badges.forEach((el) => el.remove());\n return count;\n}";
|
|
20
|
-
/**
|
|
21
|
-
* Upstream evaluate_script wraps results as prose around a ```json fence.
|
|
22
|
-
* Extract the payload array robustly.
|
|
23
|
-
*/
|
|
24
11
|
export declare function parseAnnotatedElements(text: string): AnnotatedElement[];
|
|
25
|
-
/** Render the coordinate map as compact text for tool results. */
|
|
26
12
|
export declare function formatAnnotatedMap(elements: AnnotatedElement[]): string;
|
|
27
13
|
//# sourceMappingURL=annotate.d.ts.map
|
package/dist/annotate.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Numbered-badge annotation for screenshots (adapted from the technique
|
|
3
|
-
* popularized by vercel-labs/agent-browser).
|
|
4
|
-
*
|
|
5
|
-
* Badges are injected around visible interactive elements, screenshotted,
|
|
6
|
-
* then removed. The returned coordinate map pairs each badge number with a
|
|
7
|
-
* viewport center point for coordinate click tools, plus a short label so
|
|
8
|
-
* vision models can resolve "click the blue submit button" to (x, y).
|
|
9
|
-
*/
|
|
10
1
|
export const ANNOTATE_MARKER = 'data-pi-annotate';
|
|
11
2
|
export const INJECT_ANNOTATIONS = `() => {
|
|
12
3
|
document.querySelectorAll('[${ANNOTATE_MARKER}]').forEach((el) => el.remove());
|
|
@@ -31,10 +22,6 @@ export const CLEANUP_ANNOTATIONS = `() => {
|
|
|
31
22
|
badges.forEach((el) => el.remove());
|
|
32
23
|
return count;
|
|
33
24
|
}`;
|
|
34
|
-
/**
|
|
35
|
-
* Upstream evaluate_script wraps results as prose around a ```json fence.
|
|
36
|
-
* Extract the payload array robustly.
|
|
37
|
-
*/
|
|
38
25
|
export function parseAnnotatedElements(text) {
|
|
39
26
|
const start = text.indexOf('[');
|
|
40
27
|
const end = text.lastIndexOf(']');
|
|
@@ -62,7 +49,6 @@ export function parseAnnotatedElements(text) {
|
|
|
62
49
|
return [];
|
|
63
50
|
}
|
|
64
51
|
}
|
|
65
|
-
/** Render the coordinate map as compact text for tool results. */
|
|
66
52
|
export function formatAnnotatedMap(elements) {
|
|
67
53
|
if (elements.length === 0)
|
|
68
54
|
return 'No interactive elements annotated.';
|
package/dist/artifacts.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export type ArtifactKind = 'screenshot' | 'html';
|
|
2
2
|
export declare function defaultArtifactDir(): string;
|
|
3
|
-
/** Resolve the destination file: explicit path wins, otherwise a timestamped file in the default dir. */
|
|
4
3
|
export declare function resolveArtifactTarget(kind: ArtifactKind, path?: unknown, directory?: string): string;
|
|
5
|
-
/** Pick the first image payload out of MCP content, if any. */
|
|
6
4
|
export declare function pickImageData(content: unknown): {
|
|
7
5
|
data: string;
|
|
8
6
|
mimeType: string;
|
package/dist/artifacts.js
CHANGED
|
@@ -3,13 +3,11 @@ import { join } from 'node:path';
|
|
|
3
3
|
export function defaultArtifactDir() {
|
|
4
4
|
return join(homedir(), '.pi', 'browser-artifacts');
|
|
5
5
|
}
|
|
6
|
-
/** Resolve the destination file: explicit path wins, otherwise a timestamped file in the default dir. */
|
|
7
6
|
export function resolveArtifactTarget(kind, path, directory = defaultArtifactDir()) {
|
|
8
7
|
if (typeof path === 'string' && path.length > 0)
|
|
9
8
|
return path;
|
|
10
9
|
return join(directory, `page-${Date.now()}.${kind === 'html' ? 'html' : 'png'}`);
|
|
11
10
|
}
|
|
12
|
-
/** Pick the first image payload out of MCP content, if any. */
|
|
13
11
|
export function pickImageData(content) {
|
|
14
12
|
if (!Array.isArray(content))
|
|
15
13
|
return undefined;
|
package/dist/auth-verifiers.d.ts
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill-level site authentication verifiers (spec section 6).
|
|
3
|
-
*
|
|
4
|
-
* There is intentionally no generic `isAuthenticated()` heuristic: each site
|
|
5
|
-
* presents different authenticated and unauthenticated states, so detection
|
|
6
|
-
* lives here per site. The browser subsystem only handles the resulting
|
|
7
|
-
* BrowserAuthRequired by switching Persistent into reauthentication.
|
|
8
|
-
*
|
|
9
|
-
* A verifier declares:
|
|
10
|
-
* - destination URL (where to check),
|
|
11
|
-
* - authenticated-state check,
|
|
12
|
-
* - login/challenge-state check.
|
|
13
|
-
* `ensureSiteAuthenticated` runs them against snapshot text and throws
|
|
14
|
-
* BrowserAuthRequired when a login/challenge is detected.
|
|
15
|
-
*/
|
|
16
1
|
export interface SiteAuthVerifier {
|
|
17
|
-
/** Stable provider id used in BrowserAuthRequired + status UX. */
|
|
18
2
|
provider: string;
|
|
19
|
-
/** Page to open for the auth check. */
|
|
20
3
|
destinationUrl: string;
|
|
21
|
-
/** True when the snapshot shows the authenticated state. */
|
|
22
4
|
isAuthenticated: (snapshotText: string, url: string) => boolean;
|
|
23
|
-
/** True when the snapshot shows a login/challenge wall. */
|
|
24
5
|
isLoginOrChallenge: (snapshotText: string, url: string) => boolean;
|
|
25
6
|
}
|
|
26
7
|
export interface AuthCheckPage {
|
|
@@ -36,14 +17,9 @@ export type AuthCheckResult = {
|
|
|
36
17
|
} | {
|
|
37
18
|
status: 'unknown';
|
|
38
19
|
};
|
|
39
|
-
/** Classify one snapshot without side effects. */
|
|
40
20
|
export declare function checkSiteAuth(verifier: SiteAuthVerifier, page: AuthCheckPage): AuthCheckResult;
|
|
41
|
-
/** Gmail: inbox DOM vs Google login/challenge (never conflate Chrome-profile
|
|
42
|
-
* sign-in with a mail.google.com session — they are different states). */
|
|
43
21
|
export declare const gmailVerifier: SiteAuthVerifier;
|
|
44
|
-
/** GitHub: settings/profile page shows the username when signed in. */
|
|
45
22
|
export declare const githubVerifier: SiteAuthVerifier;
|
|
46
|
-
/** Registry of built-in verifiers, keyed by provider id. */
|
|
47
23
|
export declare const BUILTIN_VERIFIERS: Record<string, SiteAuthVerifier>;
|
|
48
24
|
export interface AuthBrowser {
|
|
49
25
|
openPage(url: string): Promise<{
|
|
@@ -51,10 +27,5 @@ export interface AuthBrowser {
|
|
|
51
27
|
snapshotText: string;
|
|
52
28
|
}>;
|
|
53
29
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Open the verifier destination, classify, and either return or throw
|
|
56
|
-
* BrowserAuthRequired. `unknown` is returned (not thrown) so callers can
|
|
57
|
-
* decide — an unrecognized page is not proof of a login wall.
|
|
58
|
-
*/
|
|
59
30
|
export declare function ensureSiteAuthenticated(browser: AuthBrowser, verifier: SiteAuthVerifier): Promise<AuthCheckResult>;
|
|
60
31
|
//# sourceMappingURL=auth-verifiers.d.ts.map
|
package/dist/auth-verifiers.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill-level site authentication verifiers (spec section 6).
|
|
3
|
-
*
|
|
4
|
-
* There is intentionally no generic `isAuthenticated()` heuristic: each site
|
|
5
|
-
* presents different authenticated and unauthenticated states, so detection
|
|
6
|
-
* lives here per site. The browser subsystem only handles the resulting
|
|
7
|
-
* BrowserAuthRequired by switching Persistent into reauthentication.
|
|
8
|
-
*
|
|
9
|
-
* A verifier declares:
|
|
10
|
-
* - destination URL (where to check),
|
|
11
|
-
* - authenticated-state check,
|
|
12
|
-
* - login/challenge-state check.
|
|
13
|
-
* `ensureSiteAuthenticated` runs them against snapshot text and throws
|
|
14
|
-
* BrowserAuthRequired when a login/challenge is detected.
|
|
15
|
-
*/
|
|
16
1
|
import { BrowserAuthRequired } from './session.js';
|
|
17
|
-
/** Classify one snapshot without side effects. */
|
|
18
2
|
export function checkSiteAuth(verifier, page) {
|
|
19
3
|
if (verifier.isAuthenticated(page.snapshotText, page.url))
|
|
20
4
|
return { status: 'authenticated' };
|
|
@@ -26,13 +10,7 @@ export function checkSiteAuth(verifier, page) {
|
|
|
26
10
|
const GOOGLE_LOGIN_URL = /accounts\.google\.com\/(v\d+\/)?(signin|ServiceLogin|challenge|password|otp|verification)/i;
|
|
27
11
|
const GOOGLE_LOGIN_QUERY = /[?&](flowEntry=ServiceLogin|flowName=WebLiteSignIn|service=mail)/i;
|
|
28
12
|
const GOOGLE_CHALLENGE_COPY = /verify it'?s you|2-step verification|two-factor|enter your password|choose an account to continue|sign in to continue to gmail|to continue to gmail/i;
|
|
29
|
-
/** Raw-DOM login redirect: Gmail serves a JS hop to accounts.google.com. */
|
|
30
13
|
const GMAIL_LOGIN_BASE = /<base\s+href="https:\/\/accounts\.google\.com/i;
|
|
31
|
-
/**
|
|
32
|
-
* Inbox markers. Deliberately excludes bare "primary": Google login pages
|
|
33
|
-
* embed it in CSS custom properties (--gm3-sys-color-primary), which caused
|
|
34
|
-
* a false authenticated verdict on real sign-in HTML (live-tested 2026-09).
|
|
35
|
-
*/
|
|
36
14
|
const GMAIL_INBOX_MARKERS = [
|
|
37
15
|
/inbox/i,
|
|
38
16
|
/compose/i,
|
|
@@ -41,8 +19,6 @@ const GMAIL_INBOX_MARKERS = [
|
|
|
41
19
|
/sent mail/i,
|
|
42
20
|
/\bdrafts\b/i,
|
|
43
21
|
];
|
|
44
|
-
/** Gmail: inbox DOM vs Google login/challenge (never conflate Chrome-profile
|
|
45
|
-
* sign-in with a mail.google.com session — they are different states). */
|
|
46
22
|
export const gmailVerifier = {
|
|
47
23
|
provider: 'google',
|
|
48
24
|
destinationUrl: 'https://mail.google.com/',
|
|
@@ -61,7 +37,6 @@ export const gmailVerifier = {
|
|
|
61
37
|
GOOGLE_CHALLENGE_COPY.test(text),
|
|
62
38
|
};
|
|
63
39
|
const GITHUB_LOGIN_URL = /(^|\/)((login|session|auth)(\/|$|[?#]))/i;
|
|
64
|
-
/** GitHub: settings/profile page shows the username when signed in. */
|
|
65
40
|
export const githubVerifier = {
|
|
66
41
|
provider: 'github',
|
|
67
42
|
destinationUrl: 'https://github.com/settings/profile',
|
|
@@ -71,16 +46,10 @@ export const githubVerifier = {
|
|
|
71
46
|
GITHUB_LOGIN_URL.test(url) ||
|
|
72
47
|
(/sign in to github/i.test(text) && /username or email|password/i.test(text)),
|
|
73
48
|
};
|
|
74
|
-
/** Registry of built-in verifiers, keyed by provider id. */
|
|
75
49
|
export const BUILTIN_VERIFIERS = {
|
|
76
50
|
google: gmailVerifier,
|
|
77
51
|
github: githubVerifier,
|
|
78
52
|
};
|
|
79
|
-
/**
|
|
80
|
-
* Open the verifier destination, classify, and either return or throw
|
|
81
|
-
* BrowserAuthRequired. `unknown` is returned (not thrown) so callers can
|
|
82
|
-
* decide — an unrecognized page is not proof of a login wall.
|
|
83
|
-
*/
|
|
84
53
|
export async function ensureSiteAuthenticated(browser, verifier) {
|
|
85
54
|
const page = await browser.openPage(verifier.destinationUrl);
|
|
86
55
|
const result = checkSiteAuth(verifier, page);
|
|
@@ -1,32 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi-owned Chrome process management (spec sections 3 and 4).
|
|
3
|
-
*
|
|
4
|
-
* Normal automation launches Chrome directly with the Pi profile and an
|
|
5
|
-
* ephemeral loopback remote-debugging port, then MCP attaches via
|
|
6
|
-
* `--browser-url`. This keeps one Chrome process per profile and avoids the
|
|
7
|
-
* default WebDriver launch path where authentication often breaks:
|
|
8
|
-
*
|
|
9
|
-
* ```text
|
|
10
|
-
* Google Chrome --user-data-dir="<pi-profile>"
|
|
11
|
-
* --remote-debugging-port=<ephemeral> [--headless]
|
|
12
|
-
* ↕
|
|
13
|
-
* chrome-devtools-mcp --browser-url=http://127.0.0.1:<port>
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Bootstrap (first-run auth) launches headed Chrome *without* MCP/Puppeteer
|
|
17
|
-
* so it looks like an ordinary manually launched browser.
|
|
18
|
-
*/
|
|
19
1
|
export interface ChromeLaunchOptions {
|
|
20
2
|
userDataDir: string;
|
|
21
|
-
/** Named profile directory inside userDataDir (e.g. pi-browser-use). */
|
|
22
3
|
profileDirectory?: string;
|
|
23
|
-
/** Ephemeral loopback port. Allocated automatically when omitted. */
|
|
24
4
|
port?: number;
|
|
25
5
|
headless?: boolean;
|
|
26
|
-
/** Extra Chrome flags appended after the managed ones. */
|
|
27
6
|
chromeArgs?: string[];
|
|
28
7
|
executablePath?: string;
|
|
29
|
-
/** How long to wait for the DevTools endpoint. Default 15s. */
|
|
30
8
|
readyTimeoutMs?: number;
|
|
31
9
|
signal?: AbortSignal;
|
|
32
10
|
}
|
|
@@ -35,29 +13,14 @@ export interface ChromeProcess {
|
|
|
35
13
|
readonly port: number;
|
|
36
14
|
readonly browserUrl: string;
|
|
37
15
|
readonly userDataDir: string;
|
|
38
|
-
/** True once the process has exited. */
|
|
39
16
|
readonly exited: boolean;
|
|
40
|
-
/** Resolves when the process exits (bootstrap uses this: close → READY). */
|
|
41
17
|
waitForExit(): Promise<number | null>;
|
|
42
|
-
/** SIGTERM, then SIGKILL after `graceMs` if still alive. */
|
|
43
18
|
shutdown(graceMs?: number): Promise<void>;
|
|
44
19
|
}
|
|
45
|
-
/** Chrome/Chromium executable candidates by platform (stable first). */
|
|
46
20
|
export declare function chromeExecutableCandidates(): string[];
|
|
47
|
-
/** Resolve the Chrome executable: explicit path wins, else first candidate. */
|
|
48
21
|
export declare function findChromeExecutable(executablePath?: string): string;
|
|
49
|
-
/**
|
|
50
|
-
* Parse `ps -eo pid,command` output for Pi-managed Chromes on a profile
|
|
51
|
-
* root: processes carrying our user-data-dir AND the named-profile marker.
|
|
52
|
-
* Pure (testable): the ps text is injected.
|
|
53
|
-
*/
|
|
54
22
|
export declare function findManagedChromePids(psOutput: string, userDataDir: string, keepPid?: number): number[];
|
|
55
|
-
/** Allocate a free loopback port (never hardcode 9222). */
|
|
56
23
|
export declare function allocateEphemeralPort(): Promise<number>;
|
|
57
|
-
/**
|
|
58
|
-
* Managed Chrome flags. Headed bootstrap omits `--headless` and debugging
|
|
59
|
-
* entirely when `debugPort` is 0 (plain manual browser, spec section 3).
|
|
60
|
-
*/
|
|
61
24
|
export declare function buildChromeArgs(options: {
|
|
62
25
|
userDataDir: string;
|
|
63
26
|
profileDirectory?: string;
|
|
@@ -65,7 +28,6 @@ export declare function buildChromeArgs(options: {
|
|
|
65
28
|
headless?: boolean;
|
|
66
29
|
chromeArgs?: string[];
|
|
67
30
|
}): string[];
|
|
68
|
-
/** Poll the DevTools `/json/version` endpoint until it answers or times out. */
|
|
69
31
|
export declare function waitForDevToolsEndpoint(port: number, options?: {
|
|
70
32
|
timeoutMs?: number;
|
|
71
33
|
fetchImpl?: typeof fetch;
|
|
@@ -74,31 +36,10 @@ export declare function waitForDevToolsEndpoint(port: number, options?: {
|
|
|
74
36
|
browserUrl: string;
|
|
75
37
|
webSocketDebuggerUrl: string;
|
|
76
38
|
}>;
|
|
77
|
-
/**
|
|
78
|
-
* Launch Pi-owned Chrome. The caller must hold the profile lock
|
|
79
|
-
* (see `profile-lock.ts`) for `userDataDir` before calling.
|
|
80
|
-
*/
|
|
81
39
|
export declare function launchChrome(options: ChromeLaunchOptions): Promise<ChromeProcess>;
|
|
82
|
-
/**
|
|
83
|
-
* AppleScript that raises the exact Chrome process by pid. Unlike app-level
|
|
84
|
-
* `activate` (which may front the user's daily windows instead), this targets
|
|
85
|
-
* Pi-owned Chrome only. Foreground is reserved for explicit user-requested
|
|
86
|
-
* views and auth handoffs — never automation.
|
|
87
|
-
*/
|
|
88
40
|
export declare function buildFrontProcessScript(pid: number): string;
|
|
89
|
-
/** Best-effort fronting of Pi-owned Chrome (macOS only). Returns success. */
|
|
90
41
|
export declare function frontProcessByPid(pid: number | undefined, runner?: (cmd: string, args: string[]) => void): boolean;
|
|
91
|
-
/**
|
|
92
|
-
* AppleScript that raises the Chrome window holding the marker URL.
|
|
93
|
-
* Pure (testable): execution lives with the caller. Foreground is reserved
|
|
94
|
-
* for explicit user-requested views and auth handoffs — never automation.
|
|
95
|
-
*/
|
|
96
42
|
export declare function buildFocusWindowScript(markerUrl: string): string;
|
|
97
|
-
/**
|
|
98
|
-
* Launch the headed first-run/setup browser (spec section 3): same Pi
|
|
99
|
-
* profile, no MCP, no Puppeteer, no remote debugging — an ordinary manually
|
|
100
|
-
* launched Chrome. Resolves when the user closes the window.
|
|
101
|
-
*/
|
|
102
43
|
export declare function launchSetupBrowser(options: {
|
|
103
44
|
userDataDir: string;
|
|
104
45
|
profileDirectory?: string;
|
package/dist/chrome-launcher.js
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi-owned Chrome process management (spec sections 3 and 4).
|
|
3
|
-
*
|
|
4
|
-
* Normal automation launches Chrome directly with the Pi profile and an
|
|
5
|
-
* ephemeral loopback remote-debugging port, then MCP attaches via
|
|
6
|
-
* `--browser-url`. This keeps one Chrome process per profile and avoids the
|
|
7
|
-
* default WebDriver launch path where authentication often breaks:
|
|
8
|
-
*
|
|
9
|
-
* ```text
|
|
10
|
-
* Google Chrome --user-data-dir="<pi-profile>"
|
|
11
|
-
* --remote-debugging-port=<ephemeral> [--headless]
|
|
12
|
-
* ↕
|
|
13
|
-
* chrome-devtools-mcp --browser-url=http://127.0.0.1:<port>
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Bootstrap (first-run auth) launches headed Chrome *without* MCP/Puppeteer
|
|
17
|
-
* so it looks like an ordinary manually launched browser.
|
|
18
|
-
*/
|
|
19
1
|
import { execFileSync, spawn } from 'node:child_process';
|
|
20
2
|
import { existsSync } from 'node:fs';
|
|
21
3
|
import { createServer } from 'node:net';
|
|
22
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
23
|
-
/** Chrome/Chromium executable candidates by platform (stable first). */
|
|
24
5
|
export function chromeExecutableCandidates() {
|
|
25
6
|
if (process.platform === 'darwin') {
|
|
26
7
|
return [
|
|
@@ -44,7 +25,6 @@ export function chromeExecutableCandidates() {
|
|
|
44
25
|
'/snap/bin/chromium',
|
|
45
26
|
];
|
|
46
27
|
}
|
|
47
|
-
/** Resolve the Chrome executable: explicit path wins, else first candidate. */
|
|
48
28
|
export function findChromeExecutable(executablePath) {
|
|
49
29
|
if (executablePath && executablePath.length > 0) {
|
|
50
30
|
if (!existsSync(executablePath)) {
|
|
@@ -61,11 +41,6 @@ export function findChromeExecutable(executablePath) {
|
|
|
61
41
|
}
|
|
62
42
|
throw new Error('Chrome executable was not found. Install Google Chrome Stable or set executablePath.');
|
|
63
43
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Parse `ps -eo pid,command` output for Pi-managed Chromes on a profile
|
|
66
|
-
* root: processes carrying our user-data-dir AND the named-profile marker.
|
|
67
|
-
* Pure (testable): the ps text is injected.
|
|
68
|
-
*/
|
|
69
44
|
export function findManagedChromePids(psOutput, userDataDir, keepPid) {
|
|
70
45
|
const pids = [];
|
|
71
46
|
for (const line of psOutput.split('\n')) {
|
|
@@ -86,7 +61,6 @@ export function findManagedChromePids(psOutput, userDataDir, keepPid) {
|
|
|
86
61
|
}
|
|
87
62
|
return pids;
|
|
88
63
|
}
|
|
89
|
-
/** Allocate a free loopback port (never hardcode 9222). */
|
|
90
64
|
export function allocateEphemeralPort() {
|
|
91
65
|
return new Promise((resolve, reject) => {
|
|
92
66
|
const server = createServer();
|
|
@@ -103,10 +77,6 @@ export function allocateEphemeralPort() {
|
|
|
103
77
|
});
|
|
104
78
|
});
|
|
105
79
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Managed Chrome flags. Headed bootstrap omits `--headless` and debugging
|
|
108
|
-
* entirely when `debugPort` is 0 (plain manual browser, spec section 3).
|
|
109
|
-
*/
|
|
110
80
|
export function buildChromeArgs(options) {
|
|
111
81
|
const args = [
|
|
112
82
|
`--user-data-dir=${options.userDataDir}`,
|
|
@@ -124,7 +94,6 @@ export function buildChromeArgs(options) {
|
|
|
124
94
|
args.push(...(options.chromeArgs ?? []));
|
|
125
95
|
return args;
|
|
126
96
|
}
|
|
127
|
-
/** Poll the DevTools `/json/version` endpoint until it answers or times out. */
|
|
128
97
|
export async function waitForDevToolsEndpoint(port, options) {
|
|
129
98
|
const timeoutMs = options?.timeoutMs ?? 15_000;
|
|
130
99
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
@@ -179,7 +148,7 @@ class OwnedChromeProcess {
|
|
|
179
148
|
waitForExit() {
|
|
180
149
|
return this.exitPromise;
|
|
181
150
|
}
|
|
182
|
-
async shutdown(graceMs =
|
|
151
|
+
async shutdown(graceMs = 10_000) {
|
|
183
152
|
if (this.exited)
|
|
184
153
|
return;
|
|
185
154
|
this.child.kill('SIGTERM');
|
|
@@ -199,10 +168,6 @@ class OwnedChromeProcess {
|
|
|
199
168
|
}
|
|
200
169
|
}
|
|
201
170
|
}
|
|
202
|
-
/**
|
|
203
|
-
* Launch Pi-owned Chrome. The caller must hold the profile lock
|
|
204
|
-
* (see `profile-lock.ts`) for `userDataDir` before calling.
|
|
205
|
-
*/
|
|
206
171
|
export async function launchChrome(options) {
|
|
207
172
|
options.signal?.throwIfAborted();
|
|
208
173
|
const executable = findChromeExecutable(options.executablePath);
|
|
@@ -217,7 +182,6 @@ export async function launchChrome(options) {
|
|
|
217
182
|
const child = spawn(executable, args, { stdio: 'ignore', detached: false });
|
|
218
183
|
await new Promise((resolve, reject) => {
|
|
219
184
|
child.on('error', reject);
|
|
220
|
-
// Give spawn a tick to surface ENOENT-style failures before probing.
|
|
221
185
|
setTimeout(resolve, 50);
|
|
222
186
|
});
|
|
223
187
|
if (child.exitCode !== null) {
|
|
@@ -234,18 +198,11 @@ export async function launchChrome(options) {
|
|
|
234
198
|
child.kill('SIGKILL');
|
|
235
199
|
}
|
|
236
200
|
catch {
|
|
237
|
-
// Already gone; the endpoint error below is what matters.
|
|
238
201
|
}
|
|
239
202
|
throw error;
|
|
240
203
|
}
|
|
241
204
|
return new OwnedChromeProcess(child, port, `http://127.0.0.1:${port}`, options.userDataDir);
|
|
242
205
|
}
|
|
243
|
-
/**
|
|
244
|
-
* AppleScript that raises the exact Chrome process by pid. Unlike app-level
|
|
245
|
-
* `activate` (which may front the user's daily windows instead), this targets
|
|
246
|
-
* Pi-owned Chrome only. Foreground is reserved for explicit user-requested
|
|
247
|
-
* views and auth handoffs — never automation.
|
|
248
|
-
*/
|
|
249
206
|
export function buildFrontProcessScript(pid) {
|
|
250
207
|
return [
|
|
251
208
|
'tell application "System Events"',
|
|
@@ -253,7 +210,6 @@ export function buildFrontProcessScript(pid) {
|
|
|
253
210
|
'end tell',
|
|
254
211
|
].join('\n');
|
|
255
212
|
}
|
|
256
|
-
/** Best-effort fronting of Pi-owned Chrome (macOS only). Returns success. */
|
|
257
213
|
export function frontProcessByPid(pid, runner) {
|
|
258
214
|
if (pid === undefined || process.platform !== 'darwin')
|
|
259
215
|
return false;
|
|
@@ -266,11 +222,6 @@ export function frontProcessByPid(pid, runner) {
|
|
|
266
222
|
return false;
|
|
267
223
|
}
|
|
268
224
|
}
|
|
269
|
-
/**
|
|
270
|
-
* AppleScript that raises the Chrome window holding the marker URL.
|
|
271
|
-
* Pure (testable): execution lives with the caller. Foreground is reserved
|
|
272
|
-
* for explicit user-requested views and auth handoffs — never automation.
|
|
273
|
-
*/
|
|
274
225
|
export function buildFocusWindowScript(markerUrl) {
|
|
275
226
|
const needle = markerUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
276
227
|
return [
|
|
@@ -287,11 +238,6 @@ export function buildFocusWindowScript(markerUrl) {
|
|
|
287
238
|
'end tell',
|
|
288
239
|
].join('\n');
|
|
289
240
|
}
|
|
290
|
-
/**
|
|
291
|
-
* Launch the headed first-run/setup browser (spec section 3): same Pi
|
|
292
|
-
* profile, no MCP, no Puppeteer, no remote debugging — an ordinary manually
|
|
293
|
-
* launched Chrome. Resolves when the user closes the window.
|
|
294
|
-
*/
|
|
295
241
|
export async function launchSetupBrowser(options) {
|
|
296
242
|
options.signal?.throwIfAborted();
|
|
297
243
|
const executable = findChromeExecutable(options.executablePath);
|
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { type BrowserUseConfig } from './config.js';
|
|
2
|
-
/**
|
|
3
|
-
* MCP client that spawns chrome-devtools-mcp as a per-session subprocess over
|
|
4
|
-
* stdio. Nothing runs persistently: connect() starts it, close() kills it.
|
|
5
|
-
*/
|
|
6
2
|
export declare class DevToolsClient {
|
|
7
3
|
private client;
|
|
8
4
|
private config;
|
package/dist/client.js
CHANGED
|
@@ -8,7 +8,6 @@ const MCP_STDERR_LIMIT = 4_096;
|
|
|
8
8
|
const MCP_SYSTEM_ERROR_CODE_PATTERN = /^(?:EACCES|EADDRINUSE|ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENOENT|ENOTEMPTY|ENOTFOUND|EPERM|ETIMEDOUT)$/;
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const chromeDevToolsMcpPackagePath = require.resolve('chrome-devtools-mcp/package.json');
|
|
11
|
-
// oxlint-disable-next-line no-unsafe-read -- package.json of a pinned dependency
|
|
12
11
|
const chromeDevToolsMcpPackage = require(chromeDevToolsMcpPackagePath);
|
|
13
12
|
const chromeDevToolsMcpBin = chromeDevToolsMcpPackage.bin?.['chrome-devtools-mcp'];
|
|
14
13
|
if (!chromeDevToolsMcpBin) {
|
|
@@ -40,10 +39,6 @@ function summarizeFailure(stderr, errorName, errorCode) {
|
|
|
40
39
|
const safeName = /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(errorName) ? errorName : 'UnknownError';
|
|
41
40
|
return `MCP transport failed (${safeName}).`;
|
|
42
41
|
}
|
|
43
|
-
/**
|
|
44
|
-
* MCP client that spawns chrome-devtools-mcp as a per-session subprocess over
|
|
45
|
-
* stdio. Nothing runs persistently: connect() starts it, close() kills it.
|
|
46
|
-
*/
|
|
47
42
|
export class DevToolsClient {
|
|
48
43
|
client = null;
|
|
49
44
|
config;
|
|
@@ -72,8 +67,6 @@ export class DevToolsClient {
|
|
|
72
67
|
}
|
|
73
68
|
async openConnection(signal) {
|
|
74
69
|
this.state = this.hasConnected ? 'reconnecting' : 'connecting';
|
|
75
|
-
// Keep the MCP SDK off the extension's eager module-evaluation path and
|
|
76
|
-
// pay its cost only when session initialization opens a connection.
|
|
77
70
|
const { Client, StdioClientTransport } = await loadMcpRuntime();
|
|
78
71
|
const args = configToArgs(this.config);
|
|
79
72
|
const generation = ++this.generation;
|
|
@@ -89,8 +82,6 @@ export class DevToolsClient {
|
|
|
89
82
|
});
|
|
90
83
|
const client = new Client({ name: 'pi-browser-use', version: '0.1.0' }, { capabilities: {} });
|
|
91
84
|
this.client = client;
|
|
92
|
-
// MCP transport implements callback properties, not EventTarget.
|
|
93
|
-
// oxlint-disable-next-line unicorn/prefer-add-event-listener
|
|
94
85
|
transport.onerror = (error) => {
|
|
95
86
|
if (generation !== this.generation)
|
|
96
87
|
return;
|
|
@@ -98,7 +89,6 @@ export class DevToolsClient {
|
|
|
98
89
|
console.error(`[pi-browser-use] chrome-devtools-mcp transport error (${transportErrorCode ?? error.name})`);
|
|
99
90
|
void this.disconnectUnhealthyClient(generation);
|
|
100
91
|
};
|
|
101
|
-
// oxlint-disable-next-line unicorn/prefer-add-event-listener
|
|
102
92
|
transport.onclose = () => this.markDisconnected(generation);
|
|
103
93
|
try {
|
|
104
94
|
await client.connect(transport, signal ? { signal, timeout: MCP_TIMEOUT_MS } : { timeout: MCP_TIMEOUT_MS, signal });
|
|
@@ -118,7 +108,6 @@ export class DevToolsClient {
|
|
|
118
108
|
await client.close();
|
|
119
109
|
}
|
|
120
110
|
catch {
|
|
121
|
-
// The failed transport may already be closed.
|
|
122
111
|
}
|
|
123
112
|
if (signal?.aborted)
|
|
124
113
|
throw error;
|
|
@@ -128,8 +117,6 @@ export class DevToolsClient {
|
|
|
128
117
|
: undefined;
|
|
129
118
|
const diagnostic = summarizeFailure(stderr, errorName, transportErrorCode ?? errorCode);
|
|
130
119
|
console.error(`[pi-browser-use] browser connection failed: ${diagnostic}`);
|
|
131
|
-
// Raw upstream causes can contain credentials; retain only the sanitized diagnostic.
|
|
132
|
-
// oxlint-disable-next-line preserve-caught-error
|
|
133
120
|
throw new Error(`Browser connection failed. ${diagnostic}`);
|
|
134
121
|
}
|
|
135
122
|
}
|
|
@@ -209,17 +196,13 @@ export class DevToolsClient {
|
|
|
209
196
|
if (signal?.aborted)
|
|
210
197
|
throw error;
|
|
211
198
|
if (this.state !== 'ready' || this.client !== client) {
|
|
212
|
-
// oxlint-disable-next-line preserve-caught-error -- upstream causes may contain secrets
|
|
213
199
|
throw new Error('Browser connection lost; retry the tool.');
|
|
214
200
|
}
|
|
215
201
|
const errorName = error instanceof Error ? error.name : 'UnknownError';
|
|
216
202
|
console.error(`[pi-browser-use] upstream tool call failed (${errorName})`);
|
|
217
|
-
// Existing mode fails most often on the consent gate: say so plainly.
|
|
218
203
|
if (this.config.sessionMode === 'existing') {
|
|
219
|
-
// oxlint-disable-next-line preserve-caught-error -- upstream causes may contain secrets
|
|
220
204
|
throw new Error('Browser tool call failed. If Chrome is showing an "Allow remote debugging?" prompt, click Allow and retry.');
|
|
221
205
|
}
|
|
222
|
-
// oxlint-disable-next-line preserve-caught-error -- upstream causes may contain secrets
|
|
223
206
|
throw new Error('Browser tool call failed.');
|
|
224
207
|
}
|
|
225
208
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
export declare const DEFAULT_PROFILE_DIR: string;
|
|
2
2
|
export type BrowserSessionMode = 'persistent' | 'isolated' | 'existing';
|
|
3
|
-
/** Vision model used by the optional analyze_screenshot tool. Must already exist in Pi's model registry. */
|
|
4
3
|
export interface VisionModelConfig {
|
|
5
4
|
provider: string;
|
|
6
5
|
model: string;
|
|
7
6
|
}
|
|
8
|
-
/** Simple mode selector. Takes precedence over sessionMode/headless when set. */
|
|
9
7
|
export type BrowserModeOption = 'fresh' | 'persistent' | 'existing';
|
|
10
|
-
/** Configuration for the chrome-devtools-mcp subprocess. Same settings key as before: "pi-browser-use". */
|
|
11
8
|
export interface BrowserUseConfig {
|
|
12
|
-
/**
|
|
13
|
-
* Simple facade: fresh (isolated clean room), persistent (saved profile
|
|
14
|
-
* with your logins), existing (attach to your running Chrome). Overrides
|
|
15
|
-
* sessionMode and headless/headed below when present.
|
|
16
|
-
*/
|
|
17
9
|
mode?: BrowserModeOption;
|
|
18
|
-
/**
|
|
19
|
-
* Show the browser window. Default false (headless) for fresh and persistent;
|
|
20
|
-
* existing is always headed. Overrides headless below when present.
|
|
21
|
-
*/
|
|
22
10
|
headed?: boolean;
|
|
23
11
|
sessionMode?: BrowserSessionMode;
|
|
24
12
|
headless?: boolean;
|
|
@@ -47,26 +35,13 @@ export interface BrowserUseConfig {
|
|
|
47
35
|
allowedUrlPattern?: string[];
|
|
48
36
|
blockedUrlPattern?: string[];
|
|
49
37
|
slim?: boolean;
|
|
50
|
-
/** Loopback port for the Existing-mode tab-broker bridge. Default 31973; set 0 to disable the bridge. */
|
|
51
38
|
tabBridgePort?: number;
|
|
52
|
-
/** First-class Chrome flags, forwarded as --chrome-arg=<flag>. Only applies when Chrome is launched by chrome-devtools-mcp (not with autoConnect/browserUrl). */
|
|
53
39
|
chromeArgs?: string[];
|
|
54
|
-
/** Raw escape hatch: extra CLI flags forwarded verbatim to chrome-devtools-mcp. */
|
|
55
40
|
extraArgs?: string[];
|
|
56
41
|
}
|
|
57
|
-
/** In-session backend target for browser_switch_mode. */
|
|
58
42
|
export type BrowserMode = 'fresh' | 'persistent' | 'existing';
|
|
59
|
-
/**
|
|
60
|
-
* Build the config for a mode switch from the session base config.
|
|
61
|
-
* fresh means an isolated clean room; persistent means the saved profile
|
|
62
|
-
* (headless unless headed is requested, so logins work without popups).
|
|
63
|
-
* Attach fields never carry across modes.
|
|
64
|
-
*/
|
|
65
43
|
export declare function resolveModeTarget(base: BrowserUseConfig, mode: BrowserMode, headed?: boolean, defaultProfileDir?: string): BrowserUseConfig;
|
|
66
|
-
/** Expand a leading ~/ in user-supplied paths (env interpolation covers ${} only). */
|
|
67
44
|
export declare function expandHome(path: string): string;
|
|
68
|
-
/** Merge user config over fresh-headless defaults. */
|
|
69
45
|
export declare function resolveConfig(config?: BrowserUseConfig, defaultProfileDir?: string): BrowserUseConfig;
|
|
70
|
-
/** Convert config into CLI flags for the chrome-devtools-mcp subprocess. */
|
|
71
46
|
export declare function configToArgs(config: BrowserUseConfig): string[];
|
|
72
47
|
//# sourceMappingURL=config.d.ts.map
|