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/config.js
CHANGED
|
@@ -2,13 +2,6 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { PI_PROFILE_NAME } from './named-profile.js';
|
|
4
4
|
export const DEFAULT_PROFILE_DIR = join(homedir(), '.pi', 'browser-profile');
|
|
5
|
-
/**
|
|
6
|
-
* Persistent headless by default: Pi-owned profile, no window, never steals
|
|
7
|
-
* focus, no consent popups (log in once via browser_setup, cookies persist).
|
|
8
|
-
* Set sessionMode "isolated" (or mode "fresh") for an anonymous clean room,
|
|
9
|
-
* or "existing" + autoConnect to drive your daily Chrome (intrusive:
|
|
10
|
-
* consent popup every session, windows may pop and steal focus).
|
|
11
|
-
*/
|
|
12
5
|
const DEFAULTS = {
|
|
13
6
|
sessionMode: 'persistent',
|
|
14
7
|
headless: true,
|
|
@@ -25,12 +18,6 @@ const DEFAULTS = {
|
|
|
25
18
|
redactNetworkHeaders: true,
|
|
26
19
|
acceptInsecureCerts: false,
|
|
27
20
|
};
|
|
28
|
-
/**
|
|
29
|
-
* Build the config for a mode switch from the session base config.
|
|
30
|
-
* fresh means an isolated clean room; persistent means the saved profile
|
|
31
|
-
* (headless unless headed is requested, so logins work without popups).
|
|
32
|
-
* Attach fields never carry across modes.
|
|
33
|
-
*/
|
|
34
21
|
export function resolveModeTarget(base, mode, headed = false, defaultProfileDir = DEFAULT_PROFILE_DIR) {
|
|
35
22
|
const { browserUrl: _browserUrl, wsEndpoint: _wsEndpoint, wsHeaders: _wsHeaders, autoConnect: _autoConnect, mode: _mode, headed: _headed, ...rest } = base;
|
|
36
23
|
void _browserUrl;
|
|
@@ -45,15 +32,12 @@ export function resolveModeTarget(base, mode, headed = false, defaultProfileDir
|
|
|
45
32
|
return { ...freshRest, sessionMode: 'isolated', headless: !headed, isolated: true };
|
|
46
33
|
}
|
|
47
34
|
if (mode === 'existing') {
|
|
48
|
-
// Attach to the user's running Chrome: drop launch-only fields so MCP
|
|
49
|
-
// auto-connects instead of trying to start or reconfigure its browser.
|
|
50
35
|
const { userDataDir: _userDataDir, isolated: _isolated, executablePath: _executablePath, chromeArgs: _chromeArgs, viewport: _viewport, ...existingRest } = rest;
|
|
51
36
|
void _userDataDir;
|
|
52
37
|
void _isolated;
|
|
53
38
|
void _executablePath;
|
|
54
39
|
void _chromeArgs;
|
|
55
40
|
void _viewport;
|
|
56
|
-
// Existing attaches to the user's visible Chrome: always headed.
|
|
57
41
|
return { ...existingRest, sessionMode: 'existing', headless: false, autoConnect: true };
|
|
58
42
|
}
|
|
59
43
|
return {
|
|
@@ -64,11 +48,9 @@ export function resolveModeTarget(base, mode, headed = false, defaultProfileDir
|
|
|
64
48
|
userDataDir: base.userDataDir ?? defaultProfileDir,
|
|
65
49
|
};
|
|
66
50
|
}
|
|
67
|
-
/** Expand a leading ~/ in user-supplied paths (env interpolation covers ${} only). */
|
|
68
51
|
export function expandHome(path) {
|
|
69
52
|
return path === '~' ? homedir() : path.startsWith('~/') ? resolve(homedir(), path.slice(2)) : path;
|
|
70
53
|
}
|
|
71
|
-
/** Merge user config over fresh-headless defaults. */
|
|
72
54
|
export function resolveConfig(config, defaultProfileDir = DEFAULT_PROFILE_DIR) {
|
|
73
55
|
const { mode, headed, ...rest } = config ?? {};
|
|
74
56
|
const resolved = { ...DEFAULTS, ...rest };
|
|
@@ -77,7 +59,6 @@ export function resolveConfig(config, defaultProfileDir = DEFAULT_PROFILE_DIR) {
|
|
|
77
59
|
if (typeof resolved.executablePath === 'string')
|
|
78
60
|
resolved.executablePath = expandHome(resolved.executablePath);
|
|
79
61
|
if (mode !== undefined) {
|
|
80
|
-
// The simple facade wins over sessionMode/headless when present.
|
|
81
62
|
if (mode === 'fresh') {
|
|
82
63
|
resolved.sessionMode = 'isolated';
|
|
83
64
|
resolved.headless = !(headed ?? false);
|
|
@@ -93,8 +74,6 @@ export function resolveConfig(config, defaultProfileDir = DEFAULT_PROFILE_DIR) {
|
|
|
93
74
|
else if (headed !== undefined) {
|
|
94
75
|
resolved.headless = !headed;
|
|
95
76
|
}
|
|
96
|
-
// Existing attaches to the user's visible Chrome: always headed, so a
|
|
97
|
-
// stale headless default never leaks into flags or status output.
|
|
98
77
|
if (resolved.sessionMode === 'existing') {
|
|
99
78
|
resolved.headless = false;
|
|
100
79
|
}
|
|
@@ -126,7 +105,6 @@ export function resolveConfig(config, defaultProfileDir = DEFAULT_PROFILE_DIR) {
|
|
|
126
105
|
}
|
|
127
106
|
return resolved;
|
|
128
107
|
}
|
|
129
|
-
/** Convert config into CLI flags for the chrome-devtools-mcp subprocess. */
|
|
130
108
|
export function configToArgs(config) {
|
|
131
109
|
const args = [];
|
|
132
110
|
const resolved = resolveConfig(config);
|
|
@@ -189,7 +167,6 @@ export function configToArgs(config) {
|
|
|
189
167
|
!resolved.browserUrl &&
|
|
190
168
|
!resolved.wsEndpoint &&
|
|
191
169
|
!resolved.autoConnect) {
|
|
192
|
-
// MCP-launched persistent Chrome must use the same named Pi profile.
|
|
193
170
|
args.push(`--chrome-arg=--profile-directory=${PI_PROFILE_NAME}`);
|
|
194
171
|
}
|
|
195
172
|
if (resolved.extraArgs)
|
package/dist/doctor.d.ts
CHANGED
|
@@ -3,11 +3,8 @@ export interface DoctorReport {
|
|
|
3
3
|
mode: string;
|
|
4
4
|
headless: boolean;
|
|
5
5
|
launchesChrome: boolean;
|
|
6
|
-
/** Who owns the Chrome process: Pi self-launch, shared peer, MCP launch, or user attach. */
|
|
7
6
|
backend?: 'pi-owned' | 'shared' | 'mcp-launched' | 'attached-external';
|
|
8
|
-
/** Agent sessions currently holding claimed pages (including self). */
|
|
9
7
|
peers?: number;
|
|
10
|
-
/** Tab-bridge base URL when the Existing broker bridge is running. */
|
|
11
8
|
bridgeUrl?: string | null;
|
|
12
9
|
profile: {
|
|
13
10
|
dir: string | null;
|
|
@@ -18,11 +15,6 @@ export interface DoctorReport {
|
|
|
18
15
|
upstreamTools: number;
|
|
19
16
|
node: string;
|
|
20
17
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Self-diagnostics for the browser setup: effective mode, whether this
|
|
23
|
-
* session launches its own Chrome or attaches elsewhere, profile health,
|
|
24
|
-
* and upstream tool availability. No pages touched.
|
|
25
|
-
*/
|
|
26
18
|
export declare function diagnose(config: BrowserUseConfig, listToolNames: () => Promise<string[]>, extra?: {
|
|
27
19
|
backend?: DoctorReport['backend'];
|
|
28
20
|
bridgeUrl?: string | null;
|
package/dist/doctor.js
CHANGED
|
@@ -15,11 +15,6 @@ function profileStatus(dir) {
|
|
|
15
15
|
return { exists, writable: false };
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Self-diagnostics for the browser setup: effective mode, whether this
|
|
20
|
-
* session launches its own Chrome or attaches elsewhere, profile health,
|
|
21
|
-
* and upstream tool availability. No pages touched.
|
|
22
|
-
*/
|
|
23
18
|
export async function diagnose(config, listToolNames, extra) {
|
|
24
19
|
const launchesChrome = !config.browserUrl && !config.wsEndpoint && !config.autoConnect;
|
|
25
20
|
const dir = config.userDataDir ?? (config.sessionMode === 'persistent' ? DEFAULT_PROFILE_DIR : null);
|
package/dist/existing-flow.d.ts
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Existing-mode page opening (spec sections 18-20).
|
|
3
|
-
*
|
|
4
|
-
* Pi-created tabs in the user's Chrome are brokered by the Pi extension
|
|
5
|
-
* (see extension/background.js) through the loopback TabBridge, then
|
|
6
|
-
* correlated with MCP's page list — never by "first tab whose URL matches".
|
|
7
|
-
*
|
|
8
|
-
* Flow:
|
|
9
|
-
* 1. snapshot MCP's page ids (before),
|
|
10
|
-
* 2. bridge.requestTab(url) → token,
|
|
11
|
-
* 3. extension creates the inactive grouped tab, completes the token,
|
|
12
|
-
* 4. bridge.waitForTab(token) → { tabId },
|
|
13
|
-
* 5. diff MCP's page list (after) against before → exact new page.
|
|
14
|
-
*
|
|
15
|
-
* When the bridge/extension is unavailable, fail clearly instead of opening
|
|
16
|
-
* unmanaged foreground tabs (spec 25).
|
|
17
|
-
*/
|
|
18
1
|
import { TabBridge } from './tab-bridge.js';
|
|
19
2
|
export interface McpPageEntry {
|
|
20
3
|
pageId: number;
|
|
21
4
|
url?: string;
|
|
22
5
|
title?: string;
|
|
23
6
|
}
|
|
24
|
-
/** URLs Pi opened or navigated to in Existing mode: the only close targets. */
|
|
25
7
|
export declare function normalizeTabUrl(url: string): string;
|
|
26
|
-
/** Refuse to close Existing-mode tabs Pi did not open (spec 19). */
|
|
27
8
|
export declare function checkExistingCloseAllowed(entries: McpPageEntry[], pageId: number, piOwnedUrls: ReadonlySet<string>): {
|
|
28
9
|
ok: true;
|
|
29
10
|
} | {
|
|
@@ -32,7 +13,6 @@ export declare function checkExistingCloseAllowed(entries: McpPageEntry[], pageI
|
|
|
32
13
|
};
|
|
33
14
|
export interface ExistingFlowDeps {
|
|
34
15
|
bridge: TabBridge;
|
|
35
|
-
/** List MCP-visible pages (browser_list_pages equivalent). */
|
|
36
16
|
listPages: () => Promise<McpPageEntry[]>;
|
|
37
17
|
requestTab?: (url: string) => string;
|
|
38
18
|
waitForTab?: (token: string, options?: {
|
|
@@ -44,22 +24,11 @@ export interface ExistingFlowDeps {
|
|
|
44
24
|
}>;
|
|
45
25
|
}
|
|
46
26
|
export interface ExistingOpenResult {
|
|
47
|
-
/** Correlation token (also the bridge token). */
|
|
48
27
|
token: string;
|
|
49
|
-
/** Chrome tab id reported by the extension. */
|
|
50
28
|
tabId?: number;
|
|
51
|
-
/** MCP page id correlated by before/after diff, when unambiguous. */
|
|
52
29
|
pageId?: number;
|
|
53
30
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Best-effort MCP page list → entries. Prefers structured content, then
|
|
56
|
-
* parses the `N: title (url)` text lines list_pages emits. Never throws:
|
|
57
|
-
* unparseable results yield an empty list (correlation then stays silent
|
|
58
|
-
* instead of guessing).
|
|
59
|
-
*/
|
|
60
31
|
export declare function parseMcpPageList(result: unknown): McpPageEntry[];
|
|
61
|
-
/** Diff page lists by id; returns the single new id, or undefined when the
|
|
62
|
-
* diff is empty or ambiguous (never guess). */
|
|
63
32
|
export declare function correlateNewPage(before: McpPageEntry[], after: McpPageEntry[]): number | undefined;
|
|
64
33
|
export declare function openExistingPage(url: string, deps: ExistingFlowDeps, options?: {
|
|
65
34
|
timeoutMs?: number;
|
package/dist/existing-flow.js
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Existing-mode page opening (spec sections 18-20).
|
|
3
|
-
*
|
|
4
|
-
* Pi-created tabs in the user's Chrome are brokered by the Pi extension
|
|
5
|
-
* (see extension/background.js) through the loopback TabBridge, then
|
|
6
|
-
* correlated with MCP's page list — never by "first tab whose URL matches".
|
|
7
|
-
*
|
|
8
|
-
* Flow:
|
|
9
|
-
* 1. snapshot MCP's page ids (before),
|
|
10
|
-
* 2. bridge.requestTab(url) → token,
|
|
11
|
-
* 3. extension creates the inactive grouped tab, completes the token,
|
|
12
|
-
* 4. bridge.waitForTab(token) → { tabId },
|
|
13
|
-
* 5. diff MCP's page list (after) against before → exact new page.
|
|
14
|
-
*
|
|
15
|
-
* When the bridge/extension is unavailable, fail clearly instead of opening
|
|
16
|
-
* unmanaged foreground tabs (spec 25).
|
|
17
|
-
*/
|
|
18
1
|
import { TabBridge } from './tab-bridge.js';
|
|
19
|
-
/** URLs Pi opened or navigated to in Existing mode: the only close targets. */
|
|
20
2
|
export function normalizeTabUrl(url) {
|
|
21
3
|
return url.endsWith('/') && url.length > 1 ? url.slice(0, -1) : url;
|
|
22
4
|
}
|
|
23
|
-
/** Refuse to close Existing-mode tabs Pi did not open (spec 19). */
|
|
24
5
|
export function checkExistingCloseAllowed(entries, pageId, piOwnedUrls) {
|
|
25
6
|
const target = entries.find((entry) => entry.pageId === pageId);
|
|
26
7
|
if (!target) {
|
|
@@ -40,12 +21,6 @@ export function checkExistingCloseAllowed(entries, pageId, piOwnedUrls) {
|
|
|
40
21
|
`Pass force:true only if the user explicitly asked for this exact tab.`,
|
|
41
22
|
};
|
|
42
23
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Best-effort MCP page list → entries. Prefers structured content, then
|
|
45
|
-
* parses the `N: title (url)` text lines list_pages emits. Never throws:
|
|
46
|
-
* unparseable results yield an empty list (correlation then stays silent
|
|
47
|
-
* instead of guessing).
|
|
48
|
-
*/
|
|
49
24
|
export function parseMcpPageList(result) {
|
|
50
25
|
const entries = [];
|
|
51
26
|
if (typeof result !== 'object' || result === null)
|
|
@@ -78,8 +53,6 @@ export function parseMcpPageList(result) {
|
|
|
78
53
|
const title = (match[2] ?? '').trim();
|
|
79
54
|
if (title)
|
|
80
55
|
entry.title = title;
|
|
81
|
-
// chrome-devtools-mcp 1.8 emits `N: URL [selected]`; retain support
|
|
82
|
-
// for the older `N: title (URL)` form as well.
|
|
83
56
|
const url = (match[3] ?? (/^(https?:|about:|file:|data:)/.test(title) ? title : '')).trim();
|
|
84
57
|
if (url)
|
|
85
58
|
entry.url = url;
|
|
@@ -88,8 +61,6 @@ export function parseMcpPageList(result) {
|
|
|
88
61
|
}
|
|
89
62
|
return entries;
|
|
90
63
|
}
|
|
91
|
-
/** Diff page lists by id; returns the single new id, or undefined when the
|
|
92
|
-
* diff is empty or ambiguous (never guess). */
|
|
93
64
|
export function correlateNewPage(before, after) {
|
|
94
65
|
const known = new Set(before.map((p) => p.pageId));
|
|
95
66
|
const fresh = after.filter((p) => !known.has(p.pageId));
|
package/dist/focus-policy.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP navigation focus policy (spec section 9, layer 1 + section 20).
|
|
3
|
-
*
|
|
4
|
-
* Every Pi-created MCP page defaults to background; every page selection
|
|
5
|
-
* defaults to no foreground activation. Foreground is reserved for explicit
|
|
6
|
-
* user requests ("show me what Pi is doing") and authentication handoffs.
|
|
7
|
-
*/
|
|
8
1
|
export declare const PI_GROUP_TITLE = "pi-browser-use";
|
|
9
2
|
export interface NewPageParams {
|
|
10
3
|
url?: string;
|
|
@@ -16,13 +9,7 @@ export interface SelectPageParams {
|
|
|
16
9
|
bringToFront?: boolean;
|
|
17
10
|
[key: string]: unknown;
|
|
18
11
|
}
|
|
19
|
-
/** Default `new_page` to background unless the caller explicitly opted out. */
|
|
20
12
|
export declare function applyNewPageDefaults<T extends NewPageParams>(params: T): T;
|
|
21
|
-
/** Default `select_page` to no foreground activation. */
|
|
22
13
|
export declare function applySelectPageDefaults<T extends SelectPageParams>(params: T): T;
|
|
23
|
-
/**
|
|
24
|
-
* Foreground is allowed only when the user explicitly asked to view the page
|
|
25
|
-
* or Pi is intentionally handing control over for authentication.
|
|
26
|
-
*/
|
|
27
14
|
export declare function isForegroundAllowed(reason: unknown): boolean;
|
|
28
15
|
//# sourceMappingURL=focus-policy.d.ts.map
|
package/dist/focus-policy.js
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP navigation focus policy (spec section 9, layer 1 + section 20).
|
|
3
|
-
*
|
|
4
|
-
* Every Pi-created MCP page defaults to background; every page selection
|
|
5
|
-
* defaults to no foreground activation. Foreground is reserved for explicit
|
|
6
|
-
* user requests ("show me what Pi is doing") and authentication handoffs.
|
|
7
|
-
*/
|
|
8
1
|
export const PI_GROUP_TITLE = 'pi-browser-use';
|
|
9
|
-
/** Default `new_page` to background unless the caller explicitly opted out. */
|
|
10
2
|
export function applyNewPageDefaults(params) {
|
|
11
3
|
if (params.background === undefined)
|
|
12
4
|
return { ...params, background: true };
|
|
13
5
|
return params;
|
|
14
6
|
}
|
|
15
|
-
/** Default `select_page` to no foreground activation. */
|
|
16
7
|
export function applySelectPageDefaults(params) {
|
|
17
8
|
if (params.bringToFront === undefined)
|
|
18
9
|
return { ...params, bringToFront: false };
|
|
19
10
|
return params;
|
|
20
11
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Foreground is allowed only when the user explicitly asked to view the page
|
|
23
|
-
* or Pi is intentionally handing control over for authentication.
|
|
24
|
-
*/
|
|
25
12
|
export function isForegroundAllowed(reason) {
|
|
26
13
|
return reason === 'user-requested-view' || reason === 'auth-handoff';
|
|
27
14
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,5 @@ interface Pi {
|
|
|
15
15
|
cwd: string;
|
|
16
16
|
} & Record<string, unknown>) => Promise<void>): void;
|
|
17
17
|
}
|
|
18
|
-
/** Native Pi owns settings/trust and model credentials, not browser behavior. */
|
|
19
18
|
export default function browserUseExtension(pi: Pi): void;
|
|
20
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import { createBrowserRuntime } from './runtime.js';
|
|
|
2
2
|
import { isProjectTrusted, loadConfig } from './settings.js';
|
|
3
3
|
import { createRegistryVisionCaller } from './vision.js';
|
|
4
4
|
export { configToArgs, resolveConfig } from './config.js';
|
|
5
|
-
/** Native Pi owns settings/trust and model credentials, not browser behavior. */
|
|
6
5
|
export default function browserUseExtension(pi) {
|
|
7
6
|
let runtime;
|
|
8
7
|
pi.on('session_start', async (_event, context) => {
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { type BrowserRuntime, type BrowserRuntimeOptions } from './runtime.js';
|
|
3
|
-
/** Only explicit host configuration is read; never .pi files or project settings. */
|
|
4
3
|
export declare function loadPortableOptions(env?: NodeJS.ProcessEnv): BrowserRuntimeOptions;
|
|
5
|
-
/** Low-level MCP adapter preserves the runtime's JSON schemas and curated tools. */
|
|
6
4
|
export declare function createBrowserMcpServer(options?: {
|
|
7
5
|
runtime?: BrowserRuntime;
|
|
8
6
|
env?: NodeJS.ProcessEnv;
|
package/dist/mcp-server.js
CHANGED
|
@@ -50,7 +50,6 @@ const validateConfig = validator.getValidator({
|
|
|
50
50
|
tabBridgePort: { type: 'integer', minimum: 0, maximum: 65535 },
|
|
51
51
|
},
|
|
52
52
|
});
|
|
53
|
-
/** Only explicit host configuration is read; never .pi files or project settings. */
|
|
54
53
|
export function loadPortableOptions(env = process.env) {
|
|
55
54
|
const dataDir = env.PI_BROWSER_USE_DATA_DIR ??
|
|
56
55
|
env.PLUGIN_DATA ??
|
|
@@ -74,7 +73,6 @@ export function loadPortableOptions(env = process.env) {
|
|
|
74
73
|
parsed = JSON.parse(raw);
|
|
75
74
|
}
|
|
76
75
|
catch (error) {
|
|
77
|
-
// JSON parse errors may include fragments of wsHeaders. Never echo those.
|
|
78
76
|
throw new Error('Browser plugin config.json is not valid JSON.', { cause: error });
|
|
79
77
|
}
|
|
80
78
|
}
|
|
@@ -96,8 +94,6 @@ export function loadPortableOptions(env = process.env) {
|
|
|
96
94
|
defaultProfileDir: join(dataDir, 'browser-profile'),
|
|
97
95
|
artifactDir: join(dataDir, 'artifacts'),
|
|
98
96
|
lazyBrowser: true,
|
|
99
|
-
// Other hosts analyze returned screenshot images using their own model.
|
|
100
|
-
// No implicit Pi credentials, sampling calls, or extra model charges.
|
|
101
97
|
visionEnabled: false,
|
|
102
98
|
};
|
|
103
99
|
}
|
|
@@ -107,7 +103,6 @@ function packageVersion() {
|
|
|
107
103
|
function reportFailure(error) {
|
|
108
104
|
console.error(`[pi-browser-use] MCP lifecycle failed (${error instanceof Error ? error.name : 'UnknownError'}).`);
|
|
109
105
|
}
|
|
110
|
-
/** Low-level MCP adapter preserves the runtime's JSON schemas and curated tools. */
|
|
111
106
|
export function createBrowserMcpServer(options = {}) {
|
|
112
107
|
const runtime = options.runtime ?? createBrowserRuntime(loadPortableOptions(options.env));
|
|
113
108
|
const server = new Server({ name: 'pi-browser-use', version: packageVersion() }, { capabilities: { tools: {} } });
|
|
@@ -137,7 +132,6 @@ export function createBrowserMcpServer(options = {}) {
|
|
|
137
132
|
}
|
|
138
133
|
const checked = validate(request.params.arguments ?? {});
|
|
139
134
|
if (!checked.valid) {
|
|
140
|
-
// Do not echo argument values (forms may contain sensitive content).
|
|
141
135
|
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${tool.name}. Follow its inputSchema.`);
|
|
142
136
|
}
|
|
143
137
|
try {
|
|
@@ -173,12 +167,9 @@ export function createBrowserMcpServer(options = {}) {
|
|
|
173
167
|
closePromise ??= shutdown();
|
|
174
168
|
return closePromise;
|
|
175
169
|
}
|
|
176
|
-
// MCP uses callback properties, not DOM EventTarget listeners.
|
|
177
|
-
// oxlint-disable-next-line unicorn/prefer-add-event-listener
|
|
178
170
|
server.onclose = () => {
|
|
179
171
|
void close().catch(reportFailure);
|
|
180
172
|
};
|
|
181
|
-
// oxlint-disable-next-line unicorn/prefer-add-event-listener
|
|
182
173
|
server.onerror = reportFailure;
|
|
183
174
|
return { server, close };
|
|
184
175
|
}
|
|
@@ -198,7 +189,6 @@ export async function runStdio() {
|
|
|
198
189
|
throw error;
|
|
199
190
|
}
|
|
200
191
|
}
|
|
201
|
-
// Canonical paths handle macOS /var vs /private/var aliases in packed installs.
|
|
202
192
|
if (process.argv[1] &&
|
|
203
193
|
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
204
194
|
if (process.argv.includes('--help')) {
|
package/dist/named-profile.d.ts
CHANGED
|
@@ -1,47 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Named Pi Chrome profile (spec: Pi's browser identity).
|
|
3
|
-
*
|
|
4
|
-
* Pi's persistent data lives in a dedicated user-data-dir root
|
|
5
|
-
* (~/.pi/browser-profile), but Chrome's human-visible profile inside it was
|
|
6
|
-
* whatever Chrome picked ("Default", "Profile 1", ...). This module pins it
|
|
7
|
-
* to a stable directory named exactly `pi-browser-use`, launched via
|
|
8
|
-
* `--profile-directory`:
|
|
9
|
-
*
|
|
10
|
-
* ```text
|
|
11
|
-
* ~/.pi/browser-profile/ <- user-data-dir root (locks, metadata)
|
|
12
|
-
* pi-browser-use/ <- the named profile (cookies, logins)
|
|
13
|
-
* Cookies, Preferences, ...
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Legacy/singleton layouts ("Default", "Profile 1") are migrated once with a
|
|
17
|
-
* plain directory rename while Chrome is stopped and the profile lock is
|
|
18
|
-
* held — same files, same machine, so Keychain-bound secrets keep working.
|
|
19
|
-
* Chrome is always launched with the explicit flag afterwards, so a stale
|
|
20
|
-
* `last_used` pointer can never resurrect the old directory.
|
|
21
|
-
*/
|
|
22
1
|
export declare const PI_PROFILE_NAME = "pi-browser-use";
|
|
23
|
-
/** Human-visible name in Chrome's profile menu/picker. */
|
|
24
2
|
export declare const PI_PROFILE_DISPLAY_NAME = "Pi Browser";
|
|
25
|
-
/**
|
|
26
|
-
* Stamp the display name on a profile directory's Preferences (best effort,
|
|
27
|
-
* Chrome stopped only). Fresh directories get a minimal Preferences file
|
|
28
|
-
* that Chrome backfills on launch; custom user-chosen names are preserved.
|
|
29
|
-
*/
|
|
30
3
|
export declare function seedDisplayName(profileDir: string): boolean;
|
|
31
|
-
/** True when an external Chrome currently holds this user-data-dir root. */
|
|
32
4
|
export declare function isChromeRunningOn(root: string): boolean;
|
|
33
5
|
export interface NamedProfile {
|
|
34
|
-
/** user-data-dir root (unchanged). */
|
|
35
6
|
root: string;
|
|
36
|
-
/** Profile directory name to pass as --profile-directory. */
|
|
37
7
|
name: string;
|
|
38
|
-
/** Legacy directory migrated, if any. */
|
|
39
8
|
migratedFrom: string | null;
|
|
40
9
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Ensure `<root>/pi-browser-use` is the live profile. Migrates the current
|
|
43
|
-
* legacy directory (last_used, else Default, else Profile 1) exactly once.
|
|
44
|
-
* The caller must hold the profile lock and Chrome must be stopped.
|
|
45
|
-
*/
|
|
46
10
|
export declare function ensureNamedProfile(root: string): NamedProfile;
|
|
47
11
|
//# sourceMappingURL=named-profile.d.ts.map
|
package/dist/named-profile.js
CHANGED
|
@@ -1,41 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Named Pi Chrome profile (spec: Pi's browser identity).
|
|
3
|
-
*
|
|
4
|
-
* Pi's persistent data lives in a dedicated user-data-dir root
|
|
5
|
-
* (~/.pi/browser-profile), but Chrome's human-visible profile inside it was
|
|
6
|
-
* whatever Chrome picked ("Default", "Profile 1", ...). This module pins it
|
|
7
|
-
* to a stable directory named exactly `pi-browser-use`, launched via
|
|
8
|
-
* `--profile-directory`:
|
|
9
|
-
*
|
|
10
|
-
* ```text
|
|
11
|
-
* ~/.pi/browser-profile/ <- user-data-dir root (locks, metadata)
|
|
12
|
-
* pi-browser-use/ <- the named profile (cookies, logins)
|
|
13
|
-
* Cookies, Preferences, ...
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Legacy/singleton layouts ("Default", "Profile 1") are migrated once with a
|
|
17
|
-
* plain directory rename while Chrome is stopped and the profile lock is
|
|
18
|
-
* held — same files, same machine, so Keychain-bound secrets keep working.
|
|
19
|
-
* Chrome is always launched with the explicit flag afterwards, so a stale
|
|
20
|
-
* `last_used` pointer can never resurrect the old directory.
|
|
21
|
-
*/
|
|
22
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
23
2
|
import { join } from 'node:path';
|
|
24
3
|
export const PI_PROFILE_NAME = 'pi-browser-use';
|
|
25
|
-
/** Human-visible name in Chrome's profile menu/picker. */
|
|
26
4
|
export const PI_PROFILE_DISPLAY_NAME = 'Pi Browser';
|
|
27
|
-
/** Generic Chrome-assigned names worth replacing with ours. */
|
|
28
5
|
const GENERIC_PROFILE_NAMES = [/^person \d+$/i, /^your chrome$/i, /^default$/i];
|
|
29
6
|
function isGenericName(name) {
|
|
30
7
|
if (typeof name !== 'string' || name.trim().length === 0)
|
|
31
8
|
return true;
|
|
32
9
|
return GENERIC_PROFILE_NAMES.some((pattern) => pattern.test(name.trim()));
|
|
33
10
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Stamp the display name on a profile directory's Preferences (best effort,
|
|
36
|
-
* Chrome stopped only). Fresh directories get a minimal Preferences file
|
|
37
|
-
* that Chrome backfills on launch; custom user-chosen names are preserved.
|
|
38
|
-
*/
|
|
39
11
|
export function seedDisplayName(profileDir) {
|
|
40
12
|
try {
|
|
41
13
|
mkdirSync(profileDir, { recursive: true });
|
|
@@ -66,9 +38,7 @@ export function seedDisplayName(profileDir) {
|
|
|
66
38
|
}
|
|
67
39
|
}
|
|
68
40
|
}
|
|
69
|
-
/** Chrome singleton markers: present while any Chrome holds the root. */
|
|
70
41
|
const SINGLETON_FILES = ['SingletonSocket', 'SingletonLock', 'SingletonCookie'];
|
|
71
|
-
/** True when an external Chrome currently holds this user-data-dir root. */
|
|
72
42
|
export function isChromeRunningOn(root) {
|
|
73
43
|
return SINGLETON_FILES.some((file) => existsSync(join(root, file)));
|
|
74
44
|
}
|
|
@@ -91,14 +61,8 @@ function writeLastUsed(root, name) {
|
|
|
91
61
|
writeFileSync(path, JSON.stringify(state));
|
|
92
62
|
}
|
|
93
63
|
catch {
|
|
94
|
-
// Best effort: the explicit launch flag decides anyway.
|
|
95
64
|
}
|
|
96
65
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Ensure `<root>/pi-browser-use` is the live profile. Migrates the current
|
|
99
|
-
* legacy directory (last_used, else Default, else Profile 1) exactly once.
|
|
100
|
-
* The caller must hold the profile lock and Chrome must be stopped.
|
|
101
|
-
*/
|
|
102
66
|
export function ensureNamedProfile(root) {
|
|
103
67
|
const target = join(root, PI_PROFILE_NAME);
|
|
104
68
|
if (existsSync(target)) {
|
|
@@ -108,7 +72,6 @@ export function ensureNamedProfile(root) {
|
|
|
108
72
|
const candidates = [readLastUsed(root), 'Default', 'Profile 1'].filter((name) => typeof name === 'string');
|
|
109
73
|
const source = candidates.find((name) => existsSync(join(root, name)));
|
|
110
74
|
if (!source) {
|
|
111
|
-
// Fresh root: seed the name now so Chrome is born Pi-branded.
|
|
112
75
|
seedDisplayName(target);
|
|
113
76
|
return { root, name: PI_PROFILE_NAME, migratedFrom: null };
|
|
114
77
|
}
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi-owned Persistent Chrome backend (spec sections 2 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` — instead of asking MCP/Puppeteer to create the browser:
|
|
7
|
-
*
|
|
8
|
-
* ```text
|
|
9
|
-
* Google Chrome --user-data-dir="<pi-profile>"
|
|
10
|
-
* --remote-debugging-port=<ephemeral> [--headless]
|
|
11
|
-
* ↕
|
|
12
|
-
* chrome-devtools-mcp --browser-url=http://127.0.0.1:<port>
|
|
13
|
-
* ```
|
|
14
|
-
*
|
|
15
|
-
* Invariants:
|
|
16
|
-
* - one Chrome process <-> one persistent profile (profile lock held for the
|
|
17
|
-
* whole backend lifetime, acquired on start, released on stop);
|
|
18
|
-
* - dynamically allocated localhost port, never hardcoded 9222;
|
|
19
|
-
* - clean shutdown (SIGTERM, then SIGKILL) before any relaunch, so headed and
|
|
20
|
-
* headless instances never overlap on the same user-data-dir.
|
|
21
|
-
*
|
|
22
|
-
* The launcher and MCP-client factory are injectable so the lifecycle is
|
|
23
|
-
* unit-testable without a real Chrome.
|
|
24
|
-
*/
|
|
25
1
|
import { type ChromeLaunchOptions, type ChromeProcess } from './chrome-launcher.js';
|
|
26
2
|
import { type ProfileLockHandle } from './profile-lock.js';
|
|
27
3
|
import { type BrowserUseConfig } from './config.js';
|
|
@@ -29,15 +5,11 @@ export interface AttachedClient {
|
|
|
29
5
|
close(): Promise<void>;
|
|
30
6
|
}
|
|
31
7
|
export interface PersistentBackendOptions {
|
|
32
|
-
/** Resolved Pi config for the persistent profile. */
|
|
33
8
|
config: BrowserUseConfig;
|
|
34
|
-
/** Headed (false = headless automation, true = visible fallback/auth). */
|
|
35
9
|
headed?: boolean;
|
|
36
|
-
/** Agent-session identity for the shared registry. Generated when omitted. */
|
|
37
10
|
sessionId?: string;
|
|
38
11
|
launch?: (options: ChromeLaunchOptions) => Promise<ChromeProcess>;
|
|
39
12
|
lock?: (profileDir: string) => ProfileLockHandle;
|
|
40
|
-
/** Build the MCP client already pointed at `browserUrl`. */
|
|
41
13
|
attachClient?: (config: BrowserUseConfig) => AttachedClient;
|
|
42
14
|
}
|
|
43
15
|
export declare class PersistentBackend {
|
|
@@ -51,49 +23,23 @@ export declare class PersistentBackend {
|
|
|
51
23
|
private readonly attachClient;
|
|
52
24
|
readonly sessionId: string;
|
|
53
25
|
constructor(options: PersistentBackendOptions);
|
|
54
|
-
/** True when this session owns the Chrome process (may restart it). */
|
|
55
26
|
get owned(): boolean;
|
|
56
27
|
profileDir(): string;
|
|
57
28
|
browserUrl(): string | undefined;
|
|
58
29
|
attached(): AttachedClient | undefined;
|
|
59
|
-
/** OS pid of the owned Chrome process, for explicit user-facing fronting. */
|
|
60
30
|
pid(): number | undefined;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* Only processes carrying our managed flags; skips our own pid and
|
|
64
|
-
* manually opened windows. Injectable runner for tests.
|
|
65
|
-
*/
|
|
31
|
+
private onChromeExit;
|
|
32
|
+
private watchChrome;
|
|
66
33
|
protected reapOrphans(profileDir: string, runner?: {
|
|
67
34
|
ps(): string;
|
|
68
35
|
kill(pid: number): void;
|
|
69
36
|
}): number[];
|
|
70
|
-
/**
|
|
71
|
-
* Start Pi-owned Chrome and prepare the MCP attach config. When an
|
|
72
|
-
* `attachClient` factory was injected, the client is created here;
|
|
73
|
-
* otherwise the caller builds its DevToolsClient from `attachConfig()`.
|
|
74
|
-
*/
|
|
75
37
|
start(signal?: AbortSignal): Promise<BrowserUseConfig>;
|
|
76
|
-
/** Effective browser URL: owned Chrome or the shared peer's. */
|
|
77
38
|
effectiveBrowserUrl(): string;
|
|
78
|
-
/**
|
|
79
|
-
* MCP attach config: same session options, but pointed at Pi-owned Chrome
|
|
80
|
-
* (or the shared peer's). `userDataDir`/`isolated` are stripped — MCP
|
|
81
|
-
* must attach, not launch.
|
|
82
|
-
*/
|
|
83
39
|
attachConfig(): BrowserUseConfig;
|
|
84
40
|
running(): boolean;
|
|
85
|
-
/**
|
|
86
|
-
* Clean shutdown. Owned: close client, quit Chrome, release lock,
|
|
87
|
-
* withdraw advert. Shared: close only our own MCP client — never touch
|
|
88
|
-
* the peer's browser.
|
|
89
|
-
*/
|
|
90
41
|
stop(): Promise<void>;
|
|
91
|
-
/** Restart into the other visibility (headless <-> headed fallback). */
|
|
92
42
|
restart(headed: boolean, signal?: AbortSignal): Promise<BrowserUseConfig>;
|
|
93
43
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Persistent mode self-launches Pi-owned Chrome (§4) unless the legacy
|
|
96
|
-
* escape hatch is set (MCP launches Chrome itself, pre-Phase-2 behavior).
|
|
97
|
-
*/
|
|
98
44
|
export declare function shouldSelfLaunch(config: BrowserUseConfig): boolean;
|
|
99
45
|
//# sourceMappingURL=persistent-backend.d.ts.map
|