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.
Files changed (55) hide show
  1. package/dist/annotate.d.ts +0 -14
  2. package/dist/annotate.js +0 -14
  3. package/dist/artifacts.d.ts +0 -2
  4. package/dist/artifacts.js +0 -2
  5. package/dist/auth-verifiers.d.ts +0 -29
  6. package/dist/auth-verifiers.js +0 -31
  7. package/dist/chrome-launcher.d.ts +0 -59
  8. package/dist/chrome-launcher.js +0 -54
  9. package/dist/client.d.ts +0 -4
  10. package/dist/client.js +0 -17
  11. package/dist/config.d.ts +0 -25
  12. package/dist/config.js +0 -23
  13. package/dist/doctor.d.ts +0 -8
  14. package/dist/doctor.js +0 -5
  15. package/dist/existing-flow.d.ts +0 -31
  16. package/dist/existing-flow.js +0 -29
  17. package/dist/focus-policy.d.ts +0 -13
  18. package/dist/focus-policy.js +0 -13
  19. package/dist/index.d.ts +0 -1
  20. package/dist/index.js +0 -1
  21. package/dist/mcp-server.d.ts +0 -2
  22. package/dist/mcp-server.js +0 -10
  23. package/dist/named-profile.d.ts +0 -36
  24. package/dist/named-profile.js +0 -37
  25. package/dist/persistent-backend.d.ts +2 -56
  26. package/dist/persistent-backend.js +30 -65
  27. package/dist/persistent-store.d.ts +0 -24
  28. package/dist/persistent-store.js +0 -24
  29. package/dist/profile-lock.d.ts +0 -21
  30. package/dist/profile-lock.js +0 -27
  31. package/dist/profile.d.ts +0 -10
  32. package/dist/profile.js +0 -14
  33. package/dist/runtime.d.ts +0 -3
  34. package/dist/runtime.js +3 -92
  35. package/dist/session-manager.d.ts +0 -48
  36. package/dist/session-manager.js +0 -46
  37. package/dist/session.d.ts +0 -42
  38. package/dist/session.js +0 -18
  39. package/dist/settings.d.ts +0 -5
  40. package/dist/settings.js +0 -5
  41. package/dist/setup-flow.d.ts +0 -41
  42. package/dist/setup-flow.js +0 -37
  43. package/dist/shared-backend.d.ts +0 -30
  44. package/dist/shared-backend.js +0 -31
  45. package/dist/tab-bridge.d.ts +0 -27
  46. package/dist/tab-bridge.js +0 -27
  47. package/dist/tool-augment.d.ts +0 -9
  48. package/dist/tool-augment.js +0 -14
  49. package/dist/vision.d.ts +0 -17
  50. package/dist/vision.js +0 -17
  51. package/docs/performance.md +2 -0
  52. package/extension/README.md +2 -0
  53. package/extension/background.js +4 -9
  54. package/package.json +2 -2
  55. 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);
@@ -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;
@@ -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));
@@ -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
@@ -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) => {
@@ -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;
@@ -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')) {
@@ -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
@@ -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
- * Kill leftover Pi-managed Chromes on this profile (crashed sessions).
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