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/runtime.js CHANGED
@@ -19,12 +19,10 @@ import { runBootstrap, runReauth } from './setup-flow.js';
19
19
  import { normalizeOrigin, rememberExecutionPreference, resolveExecutionForOrigin, } from './session-manager.js';
20
20
  import { DEFAULT_BRIDGE_PORT, TabBridge } from './tab-bridge.js';
21
21
  import { handleAnalyzeScreenshot } from './vision.js';
22
- // All upstream tools are re-exported with this prefix to avoid name collisions.
23
22
  const TOOL_PREFIX = 'browser_';
24
23
  async function callUpstream(client, name, params, signal) {
25
24
  return (await client.callTool(name, params, signal));
26
25
  }
27
- // Noisy, slow, or privileged upstream tools; skipped during registration.
28
26
  const EXCLUDED_TOOLS = new Set([
29
27
  'lighthouse_audit',
30
28
  'performance_analyze_insight',
@@ -74,12 +72,10 @@ function toToolContent(result, originalName) {
74
72
  content.push({ type: 'text', text: '' });
75
73
  return result.isError ? { content, isError: true } : { content };
76
74
  }
77
- /** Shared browser lifecycle, policy, tools, and ownership. No Pi host is required. */
78
75
  export function createBrowserRuntime(options = {}) {
79
76
  const defaultProfileDir = options.defaultProfileDir ?? DEFAULT_PROFILE_DIR;
80
77
  const artifactDir = options.artifactDir ?? defaultArtifactDir();
81
78
  let config = resolveConfig(options.config, defaultProfileDir);
82
- // Remember identity even when a fresh/existing backend deliberately drops userDataDir.
83
79
  const identityProfileDir = config.userDataDir ?? defaultProfileDir;
84
80
  if (config.sessionMode === 'isolated')
85
81
  config.userDataDir = undefined;
@@ -106,27 +102,18 @@ export function createBrowserRuntime(options = {}) {
106
102
  combined.throwIfAborted();
107
103
  return tool.execute(params, combined, context);
108
104
  });
109
- // Serializing calls prevents mode switches/reauth racing in-flight page actions.
110
105
  operations = result.then(ignoreResult, ignoreResult);
111
106
  return result;
112
107
  },
113
108
  });
114
109
  }
115
- // Plugin-owned persistent Chrome (self-launched, MCP attached via browserUrl).
116
- // Set only for persistent mode when the legacy MCP-launch path is off.
117
110
  let ownBackend;
118
- // Existing-mode tab broker bridge. Lazy; lives for the whole session.
119
111
  let bridge;
120
- // Last navigated origin: drives the per-origin headed-background fallback.
121
112
  let lastOrigin;
122
- // URLs this session opened or navigated to in Existing mode (raw + normalized): the
123
- // only close_page targets allowed there without explicit force:true.
124
113
  const ownedUrls = new Set();
125
- // This extension load's agent-session identity for the shared registry.
126
114
  const sessionId = newSessionId();
127
115
  const myOwner = { sessionId, pid: process.pid };
128
116
  const shortSession = sessionId.slice(0, 8);
129
- /** Registry home: the persistent profile, or the default home for existing. */
130
117
  function registryDir() {
131
118
  return currentMode === 'persistent' ? persistentProfileDir(config ?? {}) : defaultProfileDir;
132
119
  }
@@ -134,8 +121,6 @@ export function createBrowserRuntime(options = {}) {
134
121
  ownedUrls.add(url);
135
122
  ownedUrls.add(normalizeTabUrl(url));
136
123
  }
137
- // Tracks which identity the live backend holds, so results can suggest
138
- // escalation. 'custom' covers user-configured attach setups we did not pick.
139
124
  let currentMode = describeMode();
140
125
  function describeMode() {
141
126
  if (config.sessionMode === 'existing')
@@ -153,7 +138,6 @@ export function createBrowserRuntime(options = {}) {
153
138
  function persistentProfileDir(cfg) {
154
139
  return cfg.userDataDir ?? identityProfileDir;
155
140
  }
156
- /** Close the MCP transport and any Plugin-owned Chrome. The bridge survives. */
157
141
  async function teardownBackend() {
158
142
  backendInitialized = false;
159
143
  if (client) {
@@ -161,7 +145,6 @@ export function createBrowserRuntime(options = {}) {
161
145
  await client.close();
162
146
  }
163
147
  catch {
164
- // A half-dead transport must not block the switch.
165
148
  }
166
149
  client = undefined;
167
150
  }
@@ -170,8 +153,6 @@ export function createBrowserRuntime(options = {}) {
170
153
  await ownBackend.stop();
171
154
  }
172
155
  catch {
173
- // Shutdown is best-effort; the profile lock release inside never
174
- // throws fatally, so a new backend can still start.
175
156
  }
176
157
  ownBackend = undefined;
177
158
  }
@@ -183,14 +164,6 @@ export function createBrowserRuntime(options = {}) {
183
164
  saveSitePreferences(profileDir, prefs);
184
165
  return true;
185
166
  }
186
- /**
187
- * Rebuild the backend for a mode switch. Shared by the switch tool and
188
- * automatic escalation so both paths behave identically. Persistent
189
- * self-launches Plugin-owned Chrome (MCP attaches via browserUrl) unless
190
- * PI_BROWSER_USE_LEGACY_PERSISTENT=1. A per-origin headed-background pin
191
- * wins over a headless request so one headless-hostile site never
192
- * downgrades every site.
193
- */
194
167
  async function switchBackend(mode, headed, signal, opts) {
195
168
  const next = resolveConfig(resolveModeTarget(config, mode, headed, identityProfileDir), defaultProfileDir);
196
169
  await teardownBackend();
@@ -221,8 +194,6 @@ export function createBrowserRuntime(options = {}) {
221
194
  client = createClient(next);
222
195
  }
223
196
  await client.ensureReady(signal);
224
- // Record what actually launched (a per-origin pin may have upgraded
225
- // headless to headed-background) so status/doctor tell the truth.
226
197
  next.headless = !effectiveHeaded;
227
198
  config = next;
228
199
  currentMode = mode;
@@ -234,7 +205,6 @@ export function createBrowserRuntime(options = {}) {
234
205
  throw error;
235
206
  }
236
207
  }
237
- /** Start the Existing-mode tab broker bridge on demand. */
238
208
  async function ensureBridge() {
239
209
  if (bridge)
240
210
  return bridge;
@@ -252,17 +222,6 @@ export function createBrowserRuntime(options = {}) {
252
222
  return '';
253
223
  return '\n\nHint: this looks like a login wall in a fresh (logged-out) session. If the page needs your identity, call browser_switch_mode({"mode": "persistent"}) — a human must complete any SSO, 2FA, or passkey step, ideally headed.';
254
224
  }
255
- /**
256
- * Automatic escalation for hard blocks the agent cannot clear alone.
257
- * Returns prompt text for the agent to relay, or empty when nothing
258
- * applies. Escalates at most once per call; never loops, never retries
259
- * a challenge page, and never switches away from an attached session.
260
- *
261
- * Challenges only escalate with navigation context: a stale "Just a
262
- * moment..." shortcut tile on a New Tab snapshot must not rebuild the
263
- * backend, and a challenge on another origin than requested means the
264
- * navigation never landed there.
265
- */
266
225
  async function escalateBlockedPage(url, text, signal, context) {
267
226
  const state = classifyPageState(url, text);
268
227
  if (state === 'ok')
@@ -271,8 +230,6 @@ export function createBrowserRuntime(options = {}) {
271
230
  if (context?.tool !== 'navigate_page')
272
231
  return '';
273
232
  if (context.requestedUrl && url && !sameOrigin(context.requestedUrl, url)) {
274
- // Challenge-provider handoffs (dedicated challenge domains) still
275
- // count; anything else means the navigation never landed there.
276
233
  const host = (() => {
277
234
  try {
278
235
  return new URL(url).hostname;
@@ -296,8 +253,6 @@ export function createBrowserRuntime(options = {}) {
296
253
  }
297
254
  return await escalateToHeaded(url ?? 'this page', signal);
298
255
  }
299
- // Challenge (bot check): identity never helps; only a human-gated
300
- // headed window can clear it. Stay in the same mode.
301
256
  if (config?.headless === false) {
302
257
  return '\n\nA bot challenge is blocking this page and the browser is already visible. Complete the challenge in the window, then retry.';
303
258
  }
@@ -306,13 +261,6 @@ export function createBrowserRuntime(options = {}) {
306
261
  }
307
262
  return await escalateToHeaded(url ?? 'this page', signal);
308
263
  }
309
- /**
310
- * Rebuild the current backend headed so a human can act (log in, clear
311
- * a challenge), then tell the agent exactly what to relay. Attach
312
- * sessions are already visible and owned by the user: prompt only. The
313
- * headed window is navigated to the blocked page and fronted: auth
314
- * handoff is the one case where taking foreground is the job, not a bug.
315
- */
316
264
  async function escalateToHeaded(url, signal) {
317
265
  const mode = currentMode === 'persistent' ? 'persistent' : 'fresh';
318
266
  try {
@@ -322,16 +270,12 @@ export function createBrowserRuntime(options = {}) {
322
270
  return `\n\nBlocked on ${url} and the headed browser failed to launch (${error instanceof Error ? error.message : String(error)}). Ask the user to proceed manually.`;
323
271
  }
324
272
  if (/^https?:\/\//.test(url)) {
325
- // Best effort: the window is already open for manual navigation.
326
273
  try {
327
274
  await callUpstream(client, 'new_page', { url, background: false }, signal);
328
275
  }
329
276
  catch {
330
- // Manual navigation in the opened window covers this.
331
277
  }
332
278
  }
333
- // Front Plugin-owned Chrome so the handoff window is actually visible.
334
- // Fresh MCP-launched Chrome fronts itself; only Plugin-owned needs help.
335
279
  if (ownBackend)
336
280
  frontProcessByPid(ownBackend.pid());
337
281
  return `\n\nBlocked on ${url}: a browser window just opened on that page (same ${mode} session — previous tabs are gone, re-list pages after). Please complete the login or challenge in that window, then tell the agent to continue. Do not close the window until done.`;
@@ -339,6 +283,9 @@ export function createBrowserRuntime(options = {}) {
339
283
  async function ensureConnected(signal) {
340
284
  signal?.throwIfAborted();
341
285
  try {
286
+ if (backendInitialized && ownBackend && !ownBackend.running()) {
287
+ await teardownBackend();
288
+ }
342
289
  if (!backendInitialized) {
343
290
  await client?.close();
344
291
  if (currentMode === 'persistent' && shouldSelfLaunch(config)) {
@@ -366,8 +313,6 @@ export function createBrowserRuntime(options = {}) {
366
313
  const prefixedName = `${TOOL_PREFIX}${tool.name}`;
367
314
  const originalName = tool.name;
368
315
  const description = augmentToolDescription(prefixedName, tool.description ?? '');
369
- // close_page carries an extra force gate so Existing mode can refuse
370
- // to close tabs this session did not open (spec 19); stripped before upstream.
371
316
  const parameters = originalName === 'close_page'
372
317
  ? Type.Object({
373
318
  pageId: Type.Number({
@@ -386,9 +331,6 @@ export function createBrowserRuntime(options = {}) {
386
331
  async execute(params, signal) {
387
332
  await ensureConnected(signal);
388
333
  const browser = client;
389
- // Focus policy (headed-background / existing): Agent-created pages
390
- // open in the background and selections never take foreground
391
- // unless the caller explicitly asked. Explicit values always win.
392
334
  const effectiveParams = originalName === 'new_page'
393
335
  ? applyNewPageDefaults(params)
394
336
  : originalName === 'select_page'
@@ -396,15 +338,9 @@ export function createBrowserRuntime(options = {}) {
396
338
  : params;
397
339
  if ((originalName === 'navigate_page' || originalName === 'new_page') &&
398
340
  typeof effectiveParams.url === 'string') {
399
- // Remember the origin for the per-origin headed-background
400
- // fallback; normalizeOrigin never throws (falls back to raw).
401
341
  lastOrigin = normalizeOrigin(effectiveParams.url);
402
- // In Existing mode an agent-driven navigation marks the destination
403
- // as agent-touched for the close guard below.
404
342
  if (currentMode === 'existing' && originalName === 'navigate_page')
405
343
  trackUrl(effectiveParams.url);
406
- // Claim navigated pages for this session so peer agents in a
407
- // shared browser can tell ours apart (best effort, never fatal).
408
344
  if (originalName === 'navigate_page' &&
409
345
  (currentMode === 'persistent' || currentMode === 'existing') &&
410
346
  typeof effectiveParams.pageId === 'number') {
@@ -412,13 +348,9 @@ export function createBrowserRuntime(options = {}) {
412
348
  claimPage(registryDir(), { pageId: effectiveParams.pageId, url: effectiveParams.url }, myOwner);
413
349
  }
414
350
  catch {
415
- // Ownership is coordination metadata, not the task itself.
416
351
  }
417
352
  }
418
353
  }
419
- // Never close another session's tabs: Existing mode checks Pi URL
420
- // ownership (spec 19); a shared persistent backend checks the page
421
- // registry instead. Both yield to explicit force:true.
422
354
  if (originalName === 'close_page') {
423
355
  const { force: _force, ...closeArgs } = effectiveParams;
424
356
  void _force;
@@ -448,13 +380,10 @@ export function createBrowserRuntime(options = {}) {
448
380
  releasePages(registryDir(), { pageId: effectiveParams.pageId });
449
381
  }
450
382
  catch {
451
- // Registry hygiene never fails the close itself.
452
383
  }
453
384
  }
454
385
  return { ...toToolContent(result, originalName) };
455
386
  }
456
- // Correlate a newly opened page by ID, never by URL: other agents
457
- // may have the same URL open. Ambiguous results are left unclaimed.
458
387
  let before;
459
388
  if (originalName === 'new_page' &&
460
389
  (currentMode === 'persistent' || currentMode === 'existing')) {
@@ -469,9 +398,6 @@ export function createBrowserRuntime(options = {}) {
469
398
  if (result.isError &&
470
399
  OVERLAY_RECOVERABLE.has(originalName) &&
471
400
  looksOverlayBlocked(extractTextContent(result.content))) {
472
- // One recovery attempt: dismiss the overlay, then retry the
473
- // original call. Any failure here falls through to the
474
- // original error, which already carries a hint.
475
401
  try {
476
402
  const escapeArgs = typeof effectiveParams.pageId === 'number'
477
403
  ? { pageId: effectiveParams.pageId, key: 'Escape' }
@@ -480,7 +406,6 @@ export function createBrowserRuntime(options = {}) {
480
406
  result = await callUpstream(browser, originalName, effectiveParams, signal);
481
407
  }
482
408
  catch {
483
- // Fall through to the original result below.
484
409
  }
485
410
  }
486
411
  if (!result.isError && before) {
@@ -493,15 +418,12 @@ export function createBrowserRuntime(options = {}) {
493
418
  claimPage(registryDir(), opened, myOwner);
494
419
  }
495
420
  catch {
496
- // Ownership is coordination metadata, not the task itself.
497
421
  }
498
422
  }
499
423
  }
500
424
  const toolContent = toToolContent(result, originalName);
501
425
  if (!toolContent.isError &&
502
426
  (originalName === 'navigate_page' || originalName === 'take_snapshot')) {
503
- // Prefer the page's real URL from the snapshot; fall back to the
504
- // requested URL only when the snapshot carries none.
505
427
  const snapshotUrl = pageUrlFromSnapshot(extractTextContent(result.content));
506
428
  const requestedUrl = originalName === 'navigate_page' && typeof effectiveParams.url === 'string'
507
429
  ? effectiveParams.url
@@ -591,7 +513,6 @@ export function createBrowserRuntime(options = {}) {
591
513
  }, signal);
592
514
  }
593
515
  catch {
594
- // Badges are pointer-events:none and harmless if cleanup fails.
595
516
  }
596
517
  }
597
518
  }
@@ -667,7 +588,6 @@ export function createBrowserRuntime(options = {}) {
667
588
  ],
668
589
  };
669
590
  }
670
- // No Chrome may hold the profile while the setup window runs.
671
591
  await teardownBackend();
672
592
  await withProfileLock(profileDir, () => runBootstrap({
673
593
  profileDir,
@@ -772,7 +692,6 @@ export function createBrowserRuntime(options = {}) {
772
692
  };
773
693
  }
774
694
  if (!ownBackend && !shouldSelfLaunch(config)) {
775
- // Legacy MCP-launched persistent: headed switch is the reauth path.
776
695
  await switchBackend('persistent', true, signal);
777
696
  return {
778
697
  content: [
@@ -783,7 +702,6 @@ export function createBrowserRuntime(options = {}) {
783
702
  ],
784
703
  };
785
704
  }
786
- // Spec §7: close headless Chrome cleanly before any headed reauth.
787
705
  await teardownBackend();
788
706
  const backend = createBackend({
789
707
  config: config ?? {},
@@ -802,7 +720,6 @@ export function createBrowserRuntime(options = {}) {
802
720
  });
803
721
  const message = variant === 'plain' ? await withProfileLock(backend.profileDir(), reauth) : await reauth();
804
722
  if (variant === 'plain') {
805
- // Plain window closed by the human: resume headless automation.
806
723
  const attach = await backend.restart(false, signal);
807
724
  client = createClient(attach);
808
725
  await client.ensureReady(signal);
@@ -875,7 +792,6 @@ export function createBrowserRuntime(options = {}) {
875
792
  claimPage(registryDir(), { pageId: result.pageId, url: params.url }, myOwner);
876
793
  }
877
794
  catch {
878
- // Ownership is coordination metadata, not the task itself.
879
795
  }
880
796
  }
881
797
  const selectHint = result.pageId !== undefined
@@ -947,8 +863,6 @@ export function createBrowserRuntime(options = {}) {
947
863
  }
948
864
  async function initialize() {
949
865
  if (options.lazyBrowser) {
950
- // Tool discovery is browser-free even when the chosen identity is persistent.
951
- // The upstream server launches/attaches Chrome only on its first browser call.
952
866
  client = createClient({
953
867
  ...config,
954
868
  sessionMode: 'isolated',
@@ -991,13 +905,11 @@ export function createBrowserRuntime(options = {}) {
991
905
  lifetime.abort(new Error('Browser runtime is stopped.'));
992
906
  await Promise.allSettled([startPromise, operations]);
993
907
  await teardownBackend();
994
- // Release only this runtime's claims; never terminate a borrowed browser.
995
908
  for (const scope of new Set([identityProfileDir, defaultProfileDir])) {
996
909
  try {
997
910
  releasePages(scope, { sessionId });
998
911
  }
999
912
  catch {
1000
- // Best effort.
1001
913
  }
1002
914
  }
1003
915
  if (bridge) {
@@ -1005,7 +917,6 @@ export function createBrowserRuntime(options = {}) {
1005
917
  await bridge.stop();
1006
918
  }
1007
919
  catch {
1008
- // Session teardown is best-effort.
1009
920
  }
1010
921
  bridge = undefined;
1011
922
  }
@@ -1,32 +1,11 @@
1
- /**
2
- * BrowserSessionManager: mode-specific behavior behind one stable API
3
- * (spec sections 1, 8, 21, 24, 25).
4
- *
5
- * ```text
6
- * Pi Skills / Agent → BrowserSessionManager → Fresh | Persistent | Existing
7
- * ```
8
- *
9
- * - Fresh: temporary profile, headless, deleted afterwards (spec 12).
10
- * - Persistent: Pi-owned profile with headless-first automation and a
11
- * headed-background fallback per origin (spec 2, 4, 7, 8, 24).
12
- * - Existing: the user's own Chrome via autoConnect; Pi tabs are brokered by
13
- * the Pi extension into the collapsed `pi-browser-use` group (spec 13-20).
14
- *
15
- * This module holds the state machines, capability resolution, and fallback
16
- * hierarchies. Process launching (chrome-launcher), locking (profile-lock),
17
- * and durable metadata (persistent-store) are injected collaborators so the
18
- * logic stays unit-testable without a real Chrome.
19
- */
20
1
  import type { BrowserCapabilityRequest, BrowserSession, BrowserSessionMode, BrowserStatus, PageHandle, PersistentBrowserMetadata, PersistentExecutionMode, PersistentState, SiteBrowserPreference } from './session.js';
21
2
  export type { BrowserSession, BrowserStatus, PageHandle } from './session.js';
22
- /** Minimal transport SPI a session uses to drive pages (MCP/CDP/extension). */
23
3
  export interface SessionTransport {
24
4
  openPage(url: string): Promise<PageHandle>;
25
5
  listPages(): Promise<PageHandle[]>;
26
6
  closePage(page: PageHandle): Promise<void>;
27
7
  requestVisibleBrowser?(reason: string): Promise<void>;
28
8
  }
29
- /** In-memory transport for tests and for wiring custom backends. */
30
9
  export declare function createMemoryTransport(): SessionTransport & {
31
10
  pages: PageHandle[];
32
11
  };
@@ -42,20 +21,14 @@ declare abstract class BaseSession implements BrowserSession {
42
21
  abstract getStatus(): Promise<BrowserStatus>;
43
22
  shutdown(): Promise<void>;
44
23
  }
45
- /** Fresh: anonymous, stateless, headless. Never inherits auth (spec 12). */
46
24
  export declare class FreshSession extends BaseSession {
47
25
  constructor(transport: SessionTransport);
48
26
  getStatus(): Promise<BrowserStatus>;
49
27
  }
50
- /** Events driving the Persistent state machine (spec section 2). */
51
28
  export type PersistentEvent = 'bootstrap-needed' | 'bootstrap-opened' | 'bootstrap-closed' | 'automation-started' | 'automation-succeeded' | 'auth-required' | 'reauth-opened' | 'reauth-completed' | 'headless-incompatible';
52
- /** Pure Persistent lifecycle transition. Returns the state unchanged for unknown events. */
53
29
  export declare function nextPersistentState(state: PersistentState, event: PersistentEvent): PersistentState;
54
- /** Normalize a URL or origin to a bare origin for preference lookup. */
55
30
  export declare function normalizeOrigin(urlOrOrigin: string): string;
56
- /** Resolve the execution mode for an origin or full URL (spec section 8). */
57
31
  export declare function resolveExecutionForOrigin(preferences: SiteBrowserPreference[], urlOrOrigin: string, fallback?: PersistentExecutionMode): PersistentExecutionMode;
58
- /** Remember/overwrite the per-origin preference; never downgrades globally. */
59
32
  export declare function rememberExecutionPreference(preferences: SiteBrowserPreference[], urlOrOrigin: string, executionMode: PersistentExecutionMode): SiteBrowserPreference[];
60
33
  export interface PersistentSessionOptions {
61
34
  transport: SessionTransport;
@@ -64,10 +37,6 @@ export interface PersistentSessionOptions {
64
37
  executionMode?: PersistentExecutionMode;
65
38
  sitePreferences?: SiteBrowserPreference[];
66
39
  }
67
- /**
68
- * Persistent: Pi's browser identity. Headless first; per-origin
69
- * headed-background fallback; reauth via headed browser (spec 2, 7, 8).
70
- */
71
40
  export declare class PersistentSession extends BaseSession {
72
41
  private persistentState;
73
42
  private executionMode;
@@ -79,15 +48,9 @@ export declare class PersistentSession extends BaseSession {
79
48
  get preferences(): SiteBrowserPreference[];
80
49
  send(event: PersistentEvent): PersistentState;
81
50
  executionFor(urlOrOrigin: string): PersistentExecutionMode;
82
- /** A site failed headless while headed works: pin headed-background there. */
83
51
  markHeadlessIncompatible(urlOrOrigin: string): void;
84
52
  getStatus(): Promise<BrowserStatus>;
85
53
  }
86
- /**
87
- * Existing: the user's browser identity via autoConnect (spec 13).
88
- * All Pi-created tabs must go through the extension broker into the
89
- * collapsed `pi-browser-use` group — never raw foreground tabs.
90
- */
91
54
  export declare class ExistingSession extends BaseSession {
92
55
  constructor(transport: SessionTransport);
93
56
  getStatus(): Promise<BrowserStatus>;
@@ -97,19 +60,9 @@ export interface ManagedSessions {
97
60
  persistent: BrowserSession;
98
61
  existing: BrowserSession;
99
62
  }
100
- /**
101
- * Resolve a capability request to a mode (spec section 21):
102
- * persistence/auth → persistent; explicit existing opt-in → existing;
103
- * otherwise fresh. Callers escalate along the fallback hierarchies
104
- * (sections 24/25) when the resolved mode cannot serve.
105
- */
106
63
  export declare function resolveModeForCapabilities(request: BrowserCapabilityRequest, persistentInitialized: boolean): BrowserSessionMode;
107
- /** Persistent escalation sequence (spec section 24). */
108
64
  export type PersistentEscalation = 'headless' | 'headed-auth-then-headless' | 'headed-background' | 'suggest-existing';
109
- /** Next step when persistent headless cannot proceed. */
110
65
  export declare function nextPersistentEscalation(reason: 'works' | 'login-needed' | 'headless-incompatible'): PersistentEscalation;
111
- /** Existing-mode creation contract (spec section 25): fail loudly when the
112
- * extension broker is unavailable rather than opening unmanaged tabs. */
113
66
  export declare function assertExistingBrokerAvailable(available: boolean): void;
114
67
  export declare class BrowserSessionManager {
115
68
  private readonly sessions;
@@ -118,7 +71,6 @@ export declare class BrowserSessionManager {
118
71
  getMode(): BrowserSessionMode;
119
72
  getSession(mode?: BrowserSessionMode): BrowserSession;
120
73
  switchTo(mode: BrowserSessionMode): Promise<BrowserSession>;
121
- /** Capability-based selection (spec 21): skills ask, manager resolves. */
122
74
  require(request: BrowserCapabilityRequest): Promise<BrowserSession>;
123
75
  shutdownAll(): Promise<void>;
124
76
  }
@@ -1,24 +1,4 @@
1
- /**
2
- * BrowserSessionManager: mode-specific behavior behind one stable API
3
- * (spec sections 1, 8, 21, 24, 25).
4
- *
5
- * ```text
6
- * Pi Skills / Agent → BrowserSessionManager → Fresh | Persistent | Existing
7
- * ```
8
- *
9
- * - Fresh: temporary profile, headless, deleted afterwards (spec 12).
10
- * - Persistent: Pi-owned profile with headless-first automation and a
11
- * headed-background fallback per origin (spec 2, 4, 7, 8, 24).
12
- * - Existing: the user's own Chrome via autoConnect; Pi tabs are brokered by
13
- * the Pi extension into the collapsed `pi-browser-use` group (spec 13-20).
14
- *
15
- * This module holds the state machines, capability resolution, and fallback
16
- * hierarchies. Process launching (chrome-launcher), locking (profile-lock),
17
- * and durable metadata (persistent-store) are injected collaborators so the
18
- * logic stays unit-testable without a real Chrome.
19
- */
20
1
  import { PI_GROUP_TITLE } from './focus-policy.js';
21
- /** In-memory transport for tests and for wiring custom backends. */
22
2
  export function createMemoryTransport() {
23
3
  const pages = [];
24
4
  let counter = 0;
@@ -62,7 +42,6 @@ class BaseSession {
62
42
  }
63
43
  async shutdown() { }
64
44
  }
65
- /** Fresh: anonymous, stateless, headless. Never inherits auth (spec 12). */
66
45
  export class FreshSession extends BaseSession {
67
46
  constructor(transport) {
68
47
  super('fresh', transport);
@@ -93,11 +72,9 @@ const PERSISTENT_TRANSITIONS = {
93
72
  REAUTH_REQUIRED: { 'reauth-opened': 'REAUTH_HEADFUL' },
94
73
  REAUTH_HEADFUL: { 'reauth-completed': 'READY' },
95
74
  };
96
- /** Pure Persistent lifecycle transition. Returns the state unchanged for unknown events. */
97
75
  export function nextPersistentState(state, event) {
98
76
  return PERSISTENT_TRANSITIONS[state][event] ?? state;
99
77
  }
100
- /** Normalize a URL or origin to a bare origin for preference lookup. */
101
78
  export function normalizeOrigin(urlOrOrigin) {
102
79
  try {
103
80
  return new URL(urlOrOrigin).origin;
@@ -106,23 +83,17 @@ export function normalizeOrigin(urlOrOrigin) {
106
83
  return urlOrOrigin;
107
84
  }
108
85
  }
109
- /** Resolve the execution mode for an origin or full URL (spec section 8). */
110
86
  export function resolveExecutionForOrigin(preferences, urlOrOrigin, fallback = 'headless') {
111
87
  const origin = normalizeOrigin(urlOrOrigin);
112
88
  const match = preferences.find((p) => normalizeOrigin(p.origin) === origin);
113
89
  return match?.executionMode ?? fallback;
114
90
  }
115
- /** Remember/overwrite the per-origin preference; never downgrades globally. */
116
91
  export function rememberExecutionPreference(preferences, urlOrOrigin, executionMode) {
117
92
  const origin = normalizeOrigin(urlOrOrigin);
118
93
  const next = preferences.filter((p) => normalizeOrigin(p.origin) !== origin);
119
94
  next.push({ origin, executionMode });
120
95
  return next;
121
96
  }
122
- /**
123
- * Persistent: Pi's browser identity. Headless first; per-origin
124
- * headed-background fallback; reauth via headed browser (spec 2, 7, 8).
125
- */
126
97
  export class PersistentSession extends BaseSession {
127
98
  persistentState;
128
99
  executionMode;
@@ -158,14 +129,12 @@ export class PersistentSession extends BaseSession {
158
129
  return this.executionMode;
159
130
  }
160
131
  }
161
- /** A site failed headless while headed works: pin headed-background there. */
162
132
  markHeadlessIncompatible(urlOrOrigin) {
163
133
  try {
164
134
  const origin = new URL(urlOrOrigin).origin;
165
135
  this.sitePreferences = rememberExecutionPreference(this.sitePreferences, origin, 'headed-background');
166
136
  }
167
137
  catch {
168
- // Non-URL input: fall back to the session default without pinning.
169
138
  this.executionMode = 'headed-background';
170
139
  }
171
140
  this.send('headless-incompatible');
@@ -186,11 +155,6 @@ export class PersistentSession extends BaseSession {
186
155
  };
187
156
  }
188
157
  }
189
- /**
190
- * Existing: the user's browser identity via autoConnect (spec 13).
191
- * All Pi-created tabs must go through the extension broker into the
192
- * collapsed `pi-browser-use` group — never raw foreground tabs.
193
- */
194
158
  export class ExistingSession extends BaseSession {
195
159
  constructor(transport) {
196
160
  super('existing', transport);
@@ -204,12 +168,6 @@ export class ExistingSession extends BaseSession {
204
168
  };
205
169
  }
206
170
  }
207
- /**
208
- * Resolve a capability request to a mode (spec section 21):
209
- * persistence/auth → persistent; explicit existing opt-in → existing;
210
- * otherwise fresh. Callers escalate along the fallback hierarchies
211
- * (sections 24/25) when the resolved mode cannot serve.
212
- */
213
171
  export function resolveModeForCapabilities(request, persistentInitialized) {
214
172
  if (request.persistence === true || request.authentication) {
215
173
  return 'persistent';
@@ -219,7 +177,6 @@ export function resolveModeForCapabilities(request, persistentInitialized) {
219
177
  }
220
178
  return 'fresh';
221
179
  }
222
- /** Next step when persistent headless cannot proceed. */
223
180
  export function nextPersistentEscalation(reason) {
224
181
  if (reason === 'works')
225
182
  return 'headless';
@@ -227,8 +184,6 @@ export function nextPersistentEscalation(reason) {
227
184
  return 'headed-auth-then-headless';
228
185
  return 'headed-background';
229
186
  }
230
- /** Existing-mode creation contract (spec section 25): fail loudly when the
231
- * extension broker is unavailable rather than opening unmanaged tabs. */
232
187
  export function assertExistingBrokerAvailable(available) {
233
188
  if (!available) {
234
189
  throw new Error(`Existing-mode tab broker unavailable: refusing to open unmanaged tabs. ` +
@@ -256,7 +211,6 @@ export class BrowserSessionManager {
256
211
  await session.ensureReady();
257
212
  return session;
258
213
  }
259
- /** Capability-based selection (spec 21): skills ask, manager resolves. */
260
214
  async require(request) {
261
215
  const persistent = this.sessions.persistent;
262
216
  let initialized = false;