pi-browser-use 0.11.0 → 0.11.2
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 +0 -54
- 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 +2 -0
- package/extension/README.md +2 -0
- package/extension/background.js +4 -9
- package/package.json +2 -2
- package/plugin.json +1 -1
|
@@ -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 { findManagedChromePids, launchChrome, } from './chrome-launcher.js';
|
|
26
2
|
import { execFileSync } from 'node:child_process';
|
|
27
3
|
import { ensureNamedProfile, PI_PROFILE_NAME } from './named-profile.js';
|
|
@@ -46,7 +22,6 @@ export class PersistentBackend {
|
|
|
46
22
|
this.attachClient = options.attachClient;
|
|
47
23
|
this.sessionId = options.sessionId ?? newSessionId();
|
|
48
24
|
}
|
|
49
|
-
/** True when this session owns the Chrome process (may restart it). */
|
|
50
25
|
get owned() {
|
|
51
26
|
return this.sharedAdvert === undefined;
|
|
52
27
|
}
|
|
@@ -64,15 +39,34 @@ export class PersistentBackend {
|
|
|
64
39
|
attached() {
|
|
65
40
|
return this.client;
|
|
66
41
|
}
|
|
67
|
-
/** OS pid of the owned Chrome process, for explicit user-facing fronting. */
|
|
68
42
|
pid() {
|
|
69
43
|
return this.chrome?.pid;
|
|
70
44
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
45
|
+
onChromeExit(chrome) {
|
|
46
|
+
if (this.chrome !== chrome)
|
|
47
|
+
return;
|
|
48
|
+
this.chrome = undefined;
|
|
49
|
+
const lock = this.lock;
|
|
50
|
+
this.lock = undefined;
|
|
51
|
+
try {
|
|
52
|
+
lock?.release();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
}
|
|
56
|
+
withdrawAdvert(this.profileDir(), this.sessionId);
|
|
57
|
+
const client = this.client;
|
|
58
|
+
this.client = undefined;
|
|
59
|
+
if (client)
|
|
60
|
+
void client.close().catch(() => { });
|
|
61
|
+
}
|
|
62
|
+
watchChrome(chrome) {
|
|
63
|
+
try {
|
|
64
|
+
void chrome.waitForExit().then(() => this.onChromeExit(chrome), () => this.onChromeExit(chrome));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
this.onChromeExit(chrome);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
76
70
|
reapOrphans(profileDir, runner) {
|
|
77
71
|
const run = runner ?? {
|
|
78
72
|
ps: () => {
|
|
@@ -88,7 +82,6 @@ export class PersistentBackend {
|
|
|
88
82
|
process.kill(pid, 'SIGTERM');
|
|
89
83
|
}
|
|
90
84
|
catch {
|
|
91
|
-
// Already gone; the endpoint check below confirms.
|
|
92
85
|
}
|
|
93
86
|
},
|
|
94
87
|
};
|
|
@@ -106,17 +99,13 @@ export class PersistentBackend {
|
|
|
106
99
|
reaped.push(pid);
|
|
107
100
|
}
|
|
108
101
|
catch {
|
|
109
|
-
// One stubborn process must not block the launch.
|
|
110
102
|
}
|
|
111
103
|
}
|
|
112
104
|
return reaped;
|
|
113
105
|
}
|
|
114
|
-
/**
|
|
115
|
-
* Start Pi-owned Chrome and prepare the MCP attach config. When an
|
|
116
|
-
* `attachClient` factory was injected, the client is created here;
|
|
117
|
-
* otherwise the caller builds its DevToolsClient from `attachConfig()`.
|
|
118
|
-
*/
|
|
119
106
|
async start(signal) {
|
|
107
|
+
if (this.chrome?.exited)
|
|
108
|
+
this.onChromeExit(this.chrome);
|
|
120
109
|
if (this.running())
|
|
121
110
|
return this.attachConfig();
|
|
122
111
|
if (signal?.aborted)
|
|
@@ -126,8 +115,6 @@ export class PersistentBackend {
|
|
|
126
115
|
this.lock = this.acquireLock(profileDir);
|
|
127
116
|
}
|
|
128
117
|
catch (error) {
|
|
129
|
-
// A live peer owns Chrome: share it instead of failing. No lock, no
|
|
130
|
-
// kill rights — this session only borrows pages.
|
|
131
118
|
if (error instanceof ProfileLockedError) {
|
|
132
119
|
const advert = readLiveAdvert(profileDir);
|
|
133
120
|
if (advert) {
|
|
@@ -141,20 +128,14 @@ export class PersistentBackend {
|
|
|
141
128
|
throw error;
|
|
142
129
|
}
|
|
143
130
|
try {
|
|
144
|
-
// Self-healing: reap Pi-managed orphans from crashed sessions before
|
|
145
|
-
// launching (same user-data-dir + debugging flags, never our pid,
|
|
146
|
-
// never a manually opened window). Best effort, never fatal.
|
|
147
131
|
const reaped = this.reapOrphans(profileDir);
|
|
148
132
|
if (reaped.length > 0) {
|
|
149
133
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
150
134
|
}
|
|
151
135
|
prepareBrowserProfile({ ...this.options.config, userDataDir: profileDir });
|
|
152
|
-
// Pin the named Pi profile; migrate legacy layouts once, under lock.
|
|
153
136
|
ensureNamedProfile(profileDir);
|
|
154
|
-
// Explicit headed wins; otherwise infer from config (headless:false
|
|
155
|
-
// means headed) so direct construction can't silently go headless.
|
|
156
137
|
const headed = this.options.headed ?? this.options.config.headless === false;
|
|
157
|
-
|
|
138
|
+
const chrome = await this.launch({
|
|
158
139
|
userDataDir: profileDir,
|
|
159
140
|
profileDirectory: PI_PROFILE_NAME,
|
|
160
141
|
headless: !headed,
|
|
@@ -162,6 +143,8 @@ export class PersistentBackend {
|
|
|
162
143
|
executablePath: this.options.config.executablePath,
|
|
163
144
|
signal,
|
|
164
145
|
});
|
|
146
|
+
this.chrome = chrome;
|
|
147
|
+
this.watchChrome(chrome);
|
|
165
148
|
}
|
|
166
149
|
catch (error) {
|
|
167
150
|
this.lock.release();
|
|
@@ -184,7 +167,6 @@ export class PersistentBackend {
|
|
|
184
167
|
this.client = this.attachClient(attach);
|
|
185
168
|
return attach;
|
|
186
169
|
}
|
|
187
|
-
/** Effective browser URL: owned Chrome or the shared peer's. */
|
|
188
170
|
effectiveBrowserUrl() {
|
|
189
171
|
if (this.chrome)
|
|
190
172
|
return this.chrome.browserUrl;
|
|
@@ -192,11 +174,6 @@ export class PersistentBackend {
|
|
|
192
174
|
return this.sharedAdvert.browserUrl;
|
|
193
175
|
throw new Error('Persistent backend is not running.');
|
|
194
176
|
}
|
|
195
|
-
/**
|
|
196
|
-
* MCP attach config: same session options, but pointed at Pi-owned Chrome
|
|
197
|
-
* (or the shared peer's). `userDataDir`/`isolated` are stripped — MCP
|
|
198
|
-
* must attach, not launch.
|
|
199
|
-
*/
|
|
200
177
|
attachConfig() {
|
|
201
178
|
const browserUrl = this.effectiveBrowserUrl();
|
|
202
179
|
const { userDataDir: _userDataDir, isolated: _isolated, executablePath: _executablePath, channel: _channel, chromeArgs: _chromeArgs, ...rest } = this.options.config;
|
|
@@ -205,8 +182,6 @@ export class PersistentBackend {
|
|
|
205
182
|
void _executablePath;
|
|
206
183
|
void _channel;
|
|
207
184
|
void _chromeArgs;
|
|
208
|
-
// We already launched Chrome with these options. Upstream rejects an
|
|
209
|
-
// executable/channel combined with browserUrl; attach must not relaunch.
|
|
210
185
|
return { ...rest, browserUrl, isolated: false };
|
|
211
186
|
}
|
|
212
187
|
running() {
|
|
@@ -214,11 +189,6 @@ export class PersistentBackend {
|
|
|
214
189
|
return true;
|
|
215
190
|
return this.chrome !== undefined && !this.chrome.exited;
|
|
216
191
|
}
|
|
217
|
-
/**
|
|
218
|
-
* Clean shutdown. Owned: close client, quit Chrome, release lock,
|
|
219
|
-
* withdraw advert. Shared: close only our own MCP client — never touch
|
|
220
|
-
* the peer's browser.
|
|
221
|
-
*/
|
|
222
192
|
async stop() {
|
|
223
193
|
const errors = [];
|
|
224
194
|
if (this.client) {
|
|
@@ -262,7 +232,6 @@ export class PersistentBackend {
|
|
|
262
232
|
.join('; ')}`);
|
|
263
233
|
}
|
|
264
234
|
}
|
|
265
|
-
/** Restart into the other visibility (headless <-> headed fallback). */
|
|
266
235
|
async restart(headed, signal) {
|
|
267
236
|
if (this.sharedAdvert) {
|
|
268
237
|
throw new Error('Cannot restart a shared peer backend (owned by another live session). ' +
|
|
@@ -273,10 +242,6 @@ export class PersistentBackend {
|
|
|
273
242
|
return this.start(signal);
|
|
274
243
|
}
|
|
275
244
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Persistent mode self-launches Pi-owned Chrome (§4) unless the legacy
|
|
278
|
-
* escape hatch is set (MCP launches Chrome itself, pre-Phase-2 behavior).
|
|
279
|
-
*/
|
|
280
245
|
export function shouldSelfLaunch(config) {
|
|
281
246
|
if (process.env['PI_BROWSER_USE_LEGACY_PERSISTENT'] === '1')
|
|
282
247
|
return false;
|
|
@@ -1,35 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable Persistent-mode metadata (spec section 2).
|
|
3
|
-
*
|
|
4
|
-
* Only durable state is persisted: initialization flag, profile path, last
|
|
5
|
-
* working launch mode, and last bootstrap timestamp. Passwords, cookies,
|
|
6
|
-
* tokens, or copied browser credentials must never be stored here — Chrome
|
|
7
|
-
* owns those inside its profile directory.
|
|
8
|
-
*
|
|
9
|
-
* Default location is a sibling of the profile directory:
|
|
10
|
-
* `<profileDir>.meta.json` (e.g. `~/.pi/browser-profile.meta.json`), so a
|
|
11
|
-
* profile move/rename never orphans its metadata.
|
|
12
|
-
*/
|
|
13
1
|
import type { PersistentBrowserMetadata, SiteBrowserPreference } from './session.js';
|
|
14
2
|
export declare function metadataPathFor(profileDir?: string): string;
|
|
15
3
|
export declare function defaultMetadata(profileDir?: string): PersistentBrowserMetadata;
|
|
16
|
-
/** Load metadata; returns uninitialized defaults when absent or corrupt. */
|
|
17
4
|
export declare function loadPersistentMetadata(profileDir?: string): PersistentBrowserMetadata;
|
|
18
|
-
/** Atomically persist metadata (write-then-rename via temp file). */
|
|
19
5
|
export declare function savePersistentMetadata(meta: PersistentBrowserMetadata): void;
|
|
20
|
-
/** Record a completed bootstrap: initialized now, timestamped. */
|
|
21
6
|
export declare function markBootstrapped(profileDir?: string, at?: string): PersistentBrowserMetadata;
|
|
22
|
-
/** Record which launch mode last automated successfully. */
|
|
23
7
|
export declare function markAutomationResult(profileDir: string, mode: 'headless' | 'headed'): PersistentBrowserMetadata;
|
|
24
|
-
/**
|
|
25
|
-
* Per-origin headed-background preferences (spec section 8). Stored beside
|
|
26
|
-
* the metadata file (`<profileDir>.sites.json`); origins only, never
|
|
27
|
-
* credentials. A site pinned here starts headed-background instead of
|
|
28
|
-
* globally downgrading Persistent because one site rejects headless.
|
|
29
|
-
*/
|
|
30
8
|
export declare function sitePreferencesPathFor(profileDir?: string): string;
|
|
31
|
-
/** Load per-origin preferences; empty when absent or corrupt. */
|
|
32
9
|
export declare function loadSitePreferences(profileDir?: string): SiteBrowserPreference[];
|
|
33
|
-
/** Persist per-origin preferences. */
|
|
34
10
|
export declare function saveSitePreferences(profileDir: string, preferences: SiteBrowserPreference[]): void;
|
|
35
11
|
//# sourceMappingURL=persistent-store.d.ts.map
|
package/dist/persistent-store.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable Persistent-mode metadata (spec section 2).
|
|
3
|
-
*
|
|
4
|
-
* Only durable state is persisted: initialization flag, profile path, last
|
|
5
|
-
* working launch mode, and last bootstrap timestamp. Passwords, cookies,
|
|
6
|
-
* tokens, or copied browser credentials must never be stored here — Chrome
|
|
7
|
-
* owns those inside its profile directory.
|
|
8
|
-
*
|
|
9
|
-
* Default location is a sibling of the profile directory:
|
|
10
|
-
* `<profileDir>.meta.json` (e.g. `~/.pi/browser-profile.meta.json`), so a
|
|
11
|
-
* profile move/rename never orphans its metadata.
|
|
12
|
-
*/
|
|
13
1
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
2
|
import { dirname } from 'node:path';
|
|
15
3
|
import { DEFAULT_PROFILE_DIR } from './config.js';
|
|
@@ -19,7 +7,6 @@ export function metadataPathFor(profileDir = DEFAULT_PROFILE_DIR) {
|
|
|
19
7
|
export function defaultMetadata(profileDir = DEFAULT_PROFILE_DIR) {
|
|
20
8
|
return { initialized: false, profilePath: profileDir };
|
|
21
9
|
}
|
|
22
|
-
/** Load metadata; returns uninitialized defaults when absent or corrupt. */
|
|
23
10
|
export function loadPersistentMetadata(profileDir = DEFAULT_PROFILE_DIR) {
|
|
24
11
|
const path = metadataPathFor(profileDir);
|
|
25
12
|
try {
|
|
@@ -37,13 +24,11 @@ export function loadPersistentMetadata(profileDir = DEFAULT_PROFILE_DIR) {
|
|
|
37
24
|
return defaultMetadata(profileDir);
|
|
38
25
|
}
|
|
39
26
|
}
|
|
40
|
-
/** Atomically persist metadata (write-then-rename via temp file). */
|
|
41
27
|
export function savePersistentMetadata(meta) {
|
|
42
28
|
const path = metadataPathFor(meta.profilePath);
|
|
43
29
|
mkdirSync(dirname(path), { recursive: true });
|
|
44
30
|
writeFileSync(path, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
45
31
|
}
|
|
46
|
-
/** Record a completed bootstrap: initialized now, timestamped. */
|
|
47
32
|
export function markBootstrapped(profileDir = DEFAULT_PROFILE_DIR, at = new Date().toISOString()) {
|
|
48
33
|
const meta = {
|
|
49
34
|
...loadPersistentMetadata(profileDir),
|
|
@@ -54,7 +39,6 @@ export function markBootstrapped(profileDir = DEFAULT_PROFILE_DIR, at = new Date
|
|
|
54
39
|
savePersistentMetadata(meta);
|
|
55
40
|
return meta;
|
|
56
41
|
}
|
|
57
|
-
/** Record which launch mode last automated successfully. */
|
|
58
42
|
export function markAutomationResult(profileDir, mode) {
|
|
59
43
|
const meta = {
|
|
60
44
|
...loadPersistentMetadata(profileDir),
|
|
@@ -64,16 +48,9 @@ export function markAutomationResult(profileDir, mode) {
|
|
|
64
48
|
savePersistentMetadata(meta);
|
|
65
49
|
return meta;
|
|
66
50
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Per-origin headed-background preferences (spec section 8). Stored beside
|
|
69
|
-
* the metadata file (`<profileDir>.sites.json`); origins only, never
|
|
70
|
-
* credentials. A site pinned here starts headed-background instead of
|
|
71
|
-
* globally downgrading Persistent because one site rejects headless.
|
|
72
|
-
*/
|
|
73
51
|
export function sitePreferencesPathFor(profileDir = DEFAULT_PROFILE_DIR) {
|
|
74
52
|
return `${profileDir}.sites.json`;
|
|
75
53
|
}
|
|
76
|
-
/** Load per-origin preferences; empty when absent or corrupt. */
|
|
77
54
|
export function loadSitePreferences(profileDir = DEFAULT_PROFILE_DIR) {
|
|
78
55
|
try {
|
|
79
56
|
const raw = JSON.parse(readFileSync(sitePreferencesPathFor(profileDir), 'utf8'));
|
|
@@ -89,7 +66,6 @@ export function loadSitePreferences(profileDir = DEFAULT_PROFILE_DIR) {
|
|
|
89
66
|
return [];
|
|
90
67
|
}
|
|
91
68
|
}
|
|
92
|
-
/** Persist per-origin preferences. */
|
|
93
69
|
export function saveSitePreferences(profileDir, preferences) {
|
|
94
70
|
const path = sitePreferencesPathFor(profileDir);
|
|
95
71
|
mkdirSync(dirname(path), { recursive: true });
|
package/dist/profile-lock.d.ts
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single-process guard for a Chrome user-data directory (spec section 4).
|
|
3
|
-
*
|
|
4
|
-
* Chrome corrupts state when two processes share one user-data-dir, so every
|
|
5
|
-
* Pi-owned launch/shutdown path must hold this lock:
|
|
6
|
-
*
|
|
7
|
-
* ```text
|
|
8
|
-
* one Chrome process <-> one persistent profile
|
|
9
|
-
* ```
|
|
10
|
-
*
|
|
11
|
-
* Implemented as a sibling `<profileDir>.lock` file holding `{ pid,
|
|
12
|
-
* createdAt }`. Stale locks (dead pid or age beyond `staleMs`) are reclaimed;
|
|
13
|
-
* live locks throw {@link ProfileLockedError}.
|
|
14
|
-
*/
|
|
15
1
|
export declare class ProfileLockedError extends Error {
|
|
16
2
|
readonly profileDir: string;
|
|
17
3
|
readonly holderPid?: number;
|
|
@@ -25,16 +11,9 @@ export interface ProfileLockHandle {
|
|
|
25
11
|
}
|
|
26
12
|
export declare function lockPathFor(profileDir: string): string;
|
|
27
13
|
export interface AcquireLockOptions {
|
|
28
|
-
/** Locks older than this with a dead holder are reclaimed. Default 5 min. */
|
|
29
14
|
staleMs?: number;
|
|
30
15
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Acquire the exclusive lock for `profileDir`. Throws ProfileLockedError when
|
|
33
|
-
* a live Chrome (or another Pi process) holds it.
|
|
34
|
-
*/
|
|
35
16
|
export declare function acquireProfileLock(profileDir: string, options?: AcquireLockOptions): ProfileLockHandle;
|
|
36
|
-
/** True when a live holder currently owns the profile lock. */
|
|
37
17
|
export declare function isProfileLocked(profileDir: string): boolean;
|
|
38
|
-
/** Run `fn` while holding the profile lock, always releasing afterwards. */
|
|
39
18
|
export declare function withProfileLock<T>(profileDir: string, fn: (lock: ProfileLockHandle) => Promise<T>, options?: AcquireLockOptions): Promise<T>;
|
|
40
19
|
//# sourceMappingURL=profile-lock.d.ts.map
|
package/dist/profile-lock.js
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single-process guard for a Chrome user-data directory (spec section 4).
|
|
3
|
-
*
|
|
4
|
-
* Chrome corrupts state when two processes share one user-data-dir, so every
|
|
5
|
-
* Pi-owned launch/shutdown path must hold this lock:
|
|
6
|
-
*
|
|
7
|
-
* ```text
|
|
8
|
-
* one Chrome process <-> one persistent profile
|
|
9
|
-
* ```
|
|
10
|
-
*
|
|
11
|
-
* Implemented as a sibling `<profileDir>.lock` file holding `{ pid,
|
|
12
|
-
* createdAt }`. Stale locks (dead pid or age beyond `staleMs`) are reclaimed;
|
|
13
|
-
* live locks throw {@link ProfileLockedError}.
|
|
14
|
-
*/
|
|
15
1
|
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
2
|
import { dirname } from 'node:path';
|
|
17
3
|
export class ProfileLockedError extends Error {
|
|
@@ -77,24 +63,17 @@ function tryCreateLockFile(lockPath) {
|
|
|
77
63
|
closeSync(fd);
|
|
78
64
|
}
|
|
79
65
|
catch {
|
|
80
|
-
// Best effort; the lock content is already written.
|
|
81
66
|
}
|
|
82
67
|
}
|
|
83
68
|
}
|
|
84
69
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Acquire the exclusive lock for `profileDir`. Throws ProfileLockedError when
|
|
87
|
-
* a live Chrome (or another Pi process) holds it.
|
|
88
|
-
*/
|
|
89
70
|
export function acquireProfileLock(profileDir, options) {
|
|
90
71
|
const staleMs = options?.staleMs ?? 5 * 60 * 1000;
|
|
91
72
|
const lockPath = lockPathFor(profileDir);
|
|
92
|
-
// Ensure the parent exists so the sibling lock file can be created.
|
|
93
73
|
try {
|
|
94
74
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
95
75
|
}
|
|
96
76
|
catch {
|
|
97
|
-
// Parent creation failure surfaces on lock creation below.
|
|
98
77
|
}
|
|
99
78
|
if (tryCreateLockFile(lockPath)) {
|
|
100
79
|
return makeHandle(profileDir, lockPath);
|
|
@@ -108,7 +87,6 @@ export function acquireProfileLock(profileDir, options) {
|
|
|
108
87
|
rmSync(lockPath, { force: true });
|
|
109
88
|
}
|
|
110
89
|
catch {
|
|
111
|
-
// Fall through to the error below if removal failed.
|
|
112
90
|
}
|
|
113
91
|
if (tryCreateLockFile(lockPath)) {
|
|
114
92
|
return makeHandle(profileDir, lockPath);
|
|
@@ -129,19 +107,15 @@ function makeHandle(profileDir, lockPath) {
|
|
|
129
107
|
rmSync(lockPath, { force: true });
|
|
130
108
|
}
|
|
131
109
|
catch {
|
|
132
|
-
// Lock files are best-effort on shutdown.
|
|
133
110
|
}
|
|
134
111
|
},
|
|
135
112
|
};
|
|
136
113
|
return handle;
|
|
137
114
|
}
|
|
138
|
-
/** True when a live holder currently owns the profile lock. */
|
|
139
115
|
export function isProfileLocked(profileDir) {
|
|
140
116
|
const lockPath = lockPathFor(profileDir);
|
|
141
117
|
const holderPid = readLockPid(lockPath);
|
|
142
118
|
if (holderPid === undefined) {
|
|
143
|
-
// Unparseable/missing lock file: treat a present file as locked to stay
|
|
144
|
-
// safe, absent as unlocked.
|
|
145
119
|
try {
|
|
146
120
|
readFileSync(lockPath);
|
|
147
121
|
return true;
|
|
@@ -152,7 +126,6 @@ export function isProfileLocked(profileDir) {
|
|
|
152
126
|
}
|
|
153
127
|
return isPidAlive(holderPid);
|
|
154
128
|
}
|
|
155
|
-
/** Run `fn` while holding the profile lock, always releasing afterwards. */
|
|
156
129
|
export async function withProfileLock(profileDir, fn, options) {
|
|
157
130
|
const lock = acquireProfileLock(profileDir, options);
|
|
158
131
|
try {
|
package/dist/profile.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
1
|
import { type BrowserUseConfig } from './config.js';
|
|
2
|
-
/**
|
|
3
|
-
* Guard the browser profile before launch. A default profile that exists but
|
|
4
|
-
* cannot be read/written (typically root-owned after running under sudo) is
|
|
5
|
-
* moved aside so Chrome starts fresh instead of showing a "can't read your
|
|
6
|
-
* preferences" dialog on every launch. An explicit custom userDataDir in the
|
|
7
|
-
* same state fails fast with a remediation hint instead of silently
|
|
8
|
-
* discarding the user's data. Persistent launches additionally pin the named
|
|
9
|
-
* `pi-browser-use` profile (one-time legacy migration, refuses while an
|
|
10
|
-
* external Chrome holds the root).
|
|
11
|
-
*/
|
|
12
2
|
export declare function prepareBrowserProfile(config: BrowserUseConfig): void;
|
|
13
3
|
//# sourceMappingURL=profile.d.ts.map
|
package/dist/profile.js
CHANGED
|
@@ -20,21 +20,9 @@ function isUnusableDir(path) {
|
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
|
-
// A missing directory is fine (Chrome creates it); anything else means
|
|
24
|
-
// the profile cannot be used.
|
|
25
23
|
return error.code !== 'ENOENT';
|
|
26
24
|
}
|
|
27
25
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Guard the browser profile before launch. A default profile that exists but
|
|
30
|
-
* cannot be read/written (typically root-owned after running under sudo) is
|
|
31
|
-
* moved aside so Chrome starts fresh instead of showing a "can't read your
|
|
32
|
-
* preferences" dialog on every launch. An explicit custom userDataDir in the
|
|
33
|
-
* same state fails fast with a remediation hint instead of silently
|
|
34
|
-
* discarding the user's data. Persistent launches additionally pin the named
|
|
35
|
-
* `pi-browser-use` profile (one-time legacy migration, refuses while an
|
|
36
|
-
* external Chrome holds the root).
|
|
37
|
-
*/
|
|
38
26
|
export function prepareBrowserProfile(config) {
|
|
39
27
|
if (config.browserUrl || config.wsEndpoint || config.autoConnect)
|
|
40
28
|
return;
|
|
@@ -58,8 +46,6 @@ export function prepareBrowserProfile(config) {
|
|
|
58
46
|
`Fix ownership with: sudo chown -R $(id -un):$(id -gn) ${JSON.stringify(dir)}`);
|
|
59
47
|
}
|
|
60
48
|
}
|
|
61
|
-
// Pin the named Pi profile for persistent launches (fresh migrations
|
|
62
|
-
// happen here, under no running Chrome by construction of the callers).
|
|
63
49
|
if (config.sessionMode === 'persistent') {
|
|
64
50
|
ensureNamedProfile(dir);
|
|
65
51
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -11,7 +11,6 @@ type UpstreamResult = {
|
|
|
11
11
|
}>;
|
|
12
12
|
isError?: boolean;
|
|
13
13
|
};
|
|
14
|
-
/** Host-independent tool contract; Pi and MCP adapt only at their boundaries. */
|
|
15
14
|
export type VisionCaller = Parameters<typeof handleAnalyzeScreenshot>[1];
|
|
16
15
|
export interface BrowserToolContext {
|
|
17
16
|
callVision?: VisionCaller;
|
|
@@ -30,12 +29,10 @@ export interface BrowserRuntimeOptions {
|
|
|
30
29
|
defaultProfileDir?: string;
|
|
31
30
|
artifactDir?: string;
|
|
32
31
|
visionEnabled?: boolean;
|
|
33
|
-
/** Discover tools without launching Chrome. Native Pi keeps eager startup. */
|
|
34
32
|
lazyBrowser?: boolean;
|
|
35
33
|
createClient?: (config: BrowserUseConfig) => BrowserClient;
|
|
36
34
|
createBackend?: (options: PersistentBackendOptions) => BrowserBackend;
|
|
37
35
|
}
|
|
38
|
-
/** Shared browser lifecycle, policy, tools, and ownership. No Pi host is required. */
|
|
39
36
|
export declare function createBrowserRuntime(options?: BrowserRuntimeOptions): {
|
|
40
37
|
start: () => Promise<BrowserToolDefinition[]>;
|
|
41
38
|
stop: () => Promise<void>;
|