pi-browser-use 0.6.0 → 0.7.0

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 (72) hide show
  1. package/README.md +39 -11
  2. package/dist/auth-verifiers.d.ts +60 -0
  3. package/dist/auth-verifiers.d.ts.map +1 -0
  4. package/dist/auth-verifiers.js +92 -0
  5. package/dist/auth-verifiers.js.map +1 -0
  6. package/dist/chrome-launcher.d.ts +100 -0
  7. package/dist/chrome-launcher.d.ts.map +1 -0
  8. package/dist/chrome-launcher.js +268 -0
  9. package/dist/chrome-launcher.js.map +1 -0
  10. package/dist/config.d.ts +3 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +28 -5
  13. package/dist/config.js.map +1 -1
  14. package/dist/doctor.d.ts +8 -1
  15. package/dist/doctor.d.ts.map +1 -1
  16. package/dist/doctor.js +16 -6
  17. package/dist/doctor.js.map +1 -1
  18. package/dist/existing-flow.d.ts +68 -0
  19. package/dist/existing-flow.d.ts.map +1 -0
  20. package/dist/existing-flow.js +128 -0
  21. package/dist/existing-flow.js.map +1 -0
  22. package/dist/focus-policy.d.ts +28 -0
  23. package/dist/focus-policy.d.ts.map +1 -0
  24. package/dist/focus-policy.js +28 -0
  25. package/dist/focus-policy.js.map +1 -0
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +497 -39
  29. package/dist/index.js.map +1 -1
  30. package/dist/named-profile.d.ts +47 -0
  31. package/dist/named-profile.d.ts.map +1 -0
  32. package/dist/named-profile.js +123 -0
  33. package/dist/named-profile.js.map +1 -0
  34. package/dist/persistent-backend.d.ts +77 -0
  35. package/dist/persistent-backend.d.ts.map +1 -0
  36. package/dist/persistent-backend.js +166 -0
  37. package/dist/persistent-backend.js.map +1 -0
  38. package/dist/persistent-store.d.ts +35 -0
  39. package/dist/persistent-store.d.ts.map +1 -0
  40. package/dist/persistent-store.js +98 -0
  41. package/dist/persistent-store.js.map +1 -0
  42. package/dist/profile-lock.d.ts +40 -0
  43. package/dist/profile-lock.d.ts.map +1 -0
  44. package/dist/profile-lock.js +165 -0
  45. package/dist/profile-lock.js.map +1 -0
  46. package/dist/profile.d.ts +3 -1
  47. package/dist/profile.d.ts.map +1 -1
  48. package/dist/profile.js +15 -6
  49. package/dist/profile.js.map +1 -1
  50. package/dist/session-manager.d.ts +125 -0
  51. package/dist/session-manager.d.ts.map +1 -0
  52. package/dist/session-manager.js +275 -0
  53. package/dist/session-manager.js.map +1 -0
  54. package/dist/session.d.ts +92 -0
  55. package/dist/session.d.ts.map +1 -0
  56. package/dist/session.js +29 -0
  57. package/dist/session.js.map +1 -0
  58. package/dist/setup-flow.d.ts +78 -0
  59. package/dist/setup-flow.d.ts.map +1 -0
  60. package/dist/setup-flow.js +97 -0
  61. package/dist/setup-flow.js.map +1 -0
  62. package/dist/tab-bridge.d.ts +71 -0
  63. package/dist/tab-bridge.d.ts.map +1 -0
  64. package/dist/tab-bridge.js +198 -0
  65. package/dist/tab-bridge.js.map +1 -0
  66. package/extension/README.md +33 -0
  67. package/extension/background.js +185 -0
  68. package/extension/manifest.json +12 -0
  69. package/package.json +2 -1
  70. package/skills/auth-bootstrap/SKILL.md +13 -0
  71. package/skills/browser-policy/SKILL.md +29 -4
  72. package/skills/gmail-auth/SKILL.md +51 -0
package/README.md CHANGED
@@ -4,10 +4,11 @@ Opinionated browser-use for the [Pi coding agent](https://pi.dev), powered by [`
4
4
 
5
5
  ## Why this exists
6
6
 
7
- - **Fresh headless by default** — isolated ephemeral profile, no window, never steals focus.
8
- - **Authenticated profile opt-in** — `sessionMode: persistent` persists `~/.pi/browser-profile` (log in once).
7
+ - **Persistent Pi browser by default** — self-launched headless Chrome on `~/.pi/browser-profile` (log in once via `browser_setup`); MCP attaches over an ephemeral loopback port. No window, no focus steal, no consent popups.
8
+ - **Fresh clean room on request** — `mode: fresh` for anonymous checks; nothing persists.
9
+ - **Existing-Chrome escape hatch** — `mode: existing` brokers background tabs into the collapsed `pi-browser-use` group via the bundled extension (`extension/`).
9
10
  - **CLI-first policy bundled** — `skills/browser-policy` tells agents to prefer `gh` / `wrangler` / APIs and web fetching before driving the browser.
10
- - **Safer proxying** — `browser_`-prefixed tools, noisy/privileged tools excluded, sensitive network headers redacted.
11
+ - **Safer proxying** — `browser_`-prefixed tools, noisy/privileged tools excluded, sensitive network headers redacted, background/focus-safe page defaults.
11
12
 
12
13
  Not affiliated with `@amaster.ai/pi-browser-use` or `@narumitw/pi-chrome-devtools`.
13
14
 
@@ -26,18 +27,20 @@ Settings key is `pi-browser-use` in `~/.pi/agent/settings.json` (user) or `.pi/s
26
27
  ```json
27
28
  {
28
29
  "pi-browser-use": {
29
- "mode": "fresh",
30
+ "mode": "persistent",
30
31
  "headed": false,
31
32
  "visionModel": { "provider": "openai", "model": "gpt-4o" }
32
33
  }
33
34
  }
34
35
  ```
35
36
 
36
- | `mode` | Behavior |
37
- | ----------------- | ----------------------------------------------------------- |
38
- | `fresh` (default) | Isolated clean room, thrown away each session |
39
- | `persistent` | Saved profile with your logins (`~/.pi/browser-profile`) |
40
- | `existing` | Attach to your running Chrome intrusive, avoid by default |
37
+ `persistent` is already the default; the snippet above pins it explicitly. There is no consent popup in this mode — Chrome's per-session "Allow remote debugging?" gate applies to `existing` only.
38
+
39
+ | `mode` | Behavior |
40
+ | ---------------------- | ---------------------------------------------------------------------------- |
41
+ | `persistent` (default) | Pi's own browser: saved profile with your logins (`~/.pi/browser-profile`) |
42
+ | `fresh` | Isolated clean room, thrown away each session |
43
+ | `existing` | Attach to your running Chrome — consent popup each session, avoid by default |
41
44
 
42
45
  `headed: true` shows the window (both modes default headless); `existing` is always headed. `visionModel` enables `browser_analyze_screenshot`; omit it and the tool stays unregistered.
43
46
 
@@ -68,6 +71,7 @@ Per-project identity composes: user settings set the default, trusted project se
68
71
  | `viewport` | `string` | — | Initial viewport, e.g. `1280x720` |
69
72
  | `userDataDir` | `string` | — | Custom profile dir (persistent mode defaults to `~/.pi/browser-profile`) |
70
73
  | `autoConnect` | `boolean` | `false` | Auto-attach to a local running Chrome (implies `existing`) |
74
+ | `tabBridgePort` | `number` | `31973` | Loopback port for the Existing-mode tab-broker bridge (`0` disables it) |
71
75
  | `chromeArgs` | `string[]` | — | First-class Chrome flags, forwarded as `--chrome-arg=` |
72
76
  | `extraArgs` | `string[]` | — | Raw escape hatch, forwarded verbatim to `chrome-devtools-mcp` |
73
77
  | `visionModel` | `object` | — | `{ provider, model }` from Pi's registry; enables `browser_analyze_screenshot` |
@@ -82,6 +86,8 @@ First-class `chromeArgs` only apply when `chrome-devtools-mcp` launches Chrome i
82
86
 
83
87
  Hosts where `process.execPath` is not a directly executable Node runtime can set `PI_BROWSER_USE_NODE` to the Node command used for the MCP subprocess.
84
88
 
89
+ Persistent mode self-launches Pi-owned Chrome and attaches MCP via `browserUrl`. Set `PI_BROWSER_USE_LEGACY_PERSISTENT=1` to restore the pre-Phase-2 behavior (MCP launches Chrome itself).
90
+
85
91
  ## Tools
86
92
 
87
93
  Upstream `chrome-devtools-mcp` tools are proxied as `browser_*` (navigation, snapshot, click/fill/hover/drag/type, emulation, console, network, screenshots, dialogs, uploads, waits). Noisy, slow, or privileged ones (`lighthouse_audit`, performance traces, screencast, extension management) are excluded. On top, this package adds:
@@ -105,11 +111,27 @@ browser_save_artifact({ "pageId": 1, "kind": "screenshot", "annotate": true })
105
111
 
106
112
  ### `browser_switch_mode`
107
113
 
108
- Switch backends without restarting: `fresh` (isolated clean room) or `persistent` (saved profile with your logins). Both default to headless — pass `headed: true` to watch. Tabs do not transfer; call `browser_list_pages` after switching. Prefer fresh; escalate to persistent only on login walls.
114
+ Switch backends without restarting: `fresh` (isolated clean room), `persistent` (Pi-owned Chrome on the saved profile with your logins), or `existing` (attach to your running Chrome). Fresh and persistent default to headless — pass `headed: true` to watch. Tabs do not transfer; call `browser_list_pages` after switching. Prefer fresh; escalate to persistent only on login walls. `rememberSite: true` pins the last-visited site's visibility (headless vs headed-background) for next time.
115
+
116
+ ### `browser_setup`
117
+
118
+ First-run setup for the persistent profile: opens a plain headed Chrome window (no automation attached) for a human to sign in, and completes when the window closes. If the profile is already initialized it says so and points at `browser_reauth` instead.
119
+
120
+ ### `browser_status`
121
+
122
+ Plain-language status — profile readiness, execution mode, next step — without touching any page. Bootstrap initializes the profile; it never proves a site session.
123
+
124
+ ### `browser_reauth`
125
+
126
+ Reauthenticate the persistent profile: shuts headless Chrome down cleanly, opens a headed window for the human to verify, then resumes headless. `variant: plain` opens a dependency-free window for providers that reject instrumented browsers.
127
+
128
+ ### `browser_open_background_tab`
129
+
130
+ Existing mode only: opens a URL as an inactive tab in the collapsed `pi-browser-use` group via the Pi extension (see `extension/`). Fails clearly when the extension bridge is unavailable — never a foreground tab.
109
131
 
110
132
  ### `browser_doctor`
111
133
 
112
- Self-diagnostics: effective mode, whether this session launches its own Chrome, profile health, and upstream tool availability. Run it first when browser tools misbehave — it touches no pages.
134
+ Self-diagnostics: effective mode, backend ownership (Pi-owned vs MCP-launched vs attached), profile health, tab-bridge URL, and upstream tool availability. Run it first when browser tools misbehave — it touches no pages.
113
135
 
114
136
  ### `browser_analyze_screenshot`
115
137
 
@@ -129,6 +151,8 @@ Call `browser_list_pages` first, then pass its numeric `pageId` to page-scoped t
129
151
 
130
152
  ## Browser profile
131
153
 
154
+ The persistent browser lives in a Chrome profile directory named `pi-browser-use` inside `~/.pi/browser-profile` (one-time automatic migration from legacy `Default`/`Profile 1` layouts while Chrome is stopped), so Pi's cookies and logins are always distinguishable on disk from anything else.
155
+
132
156
  On startup the default persistent profile is checked for accessibility. A root-owned or unreadable default (typically from running under `sudo`) is moved aside to `~/.pi/browser-profile.inaccessible-<timestamp>` so Chrome starts fresh instead of showing a preferences dialog. An explicit custom `userDataDir` in the same state fails fast with an ownership remediation hint. Never run the agent (or anything launching this browser) via `sudo`.
133
157
 
134
158
  ## Performance
@@ -138,6 +162,8 @@ On startup the default persistent profile is checked for accessibility. A root-o
138
162
  ## Bundled skills
139
163
 
140
164
  - `browser-policy` — CLI-first decision order, session modes, bot-wall and safety rules.
165
+ - `gmail-auth` — Gmail inbox verification, challenge handling, headless/headed pinning.
166
+ - `auth-bootstrap` — first-run login flow for the persistent profile.
141
167
  - `playwright-handoff` — when to stop clicking and run a repo Playwright spec instead (npm-first, bun alternatives, no repo changes required).
142
168
  - `triage-console` — snapshot → console errors → failed network → screenshot.
143
169
  - `visual-qa` — viewport matrix and canvas/WebGL discipline, including the annotate flow.
@@ -154,6 +180,8 @@ On startup the default persistent profile is checked for accessibility. A root-o
154
180
  | `profile is already in use` | Two sessions sharing one `userDataDir` | Use isolated mode or separate dirs per session |
155
181
  | Stale uid errors | Page re-rendered after your snapshot | Take a fresh snapshot; ids invalidate on every action |
156
182
  | Vision tool missing | No `visionModel` configured | Add it; otherwise use tree uids, or the global `vision` skill for vision-less models |
183
+ | "Allow remote debugging?" popup | Chrome 144+ consent gate for MCP auto-attach in Existing mode | Click Allow (once per Pi session); persistent mode never triggers it |
184
+ | Pi tab closed the wrong tab | Page IDs shift when tabs close — fixed | Existing `close_page` now refuses tabs Pi didn't open unless `force: true` |
157
185
 
158
186
  ## License
159
187
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Skill-level site authentication verifiers (spec section 6).
3
+ *
4
+ * There is intentionally no generic `isAuthenticated()` heuristic: each site
5
+ * presents different authenticated and unauthenticated states, so detection
6
+ * lives here per site. The browser subsystem only handles the resulting
7
+ * BrowserAuthRequired by switching Persistent into reauthentication.
8
+ *
9
+ * A verifier declares:
10
+ * - destination URL (where to check),
11
+ * - authenticated-state check,
12
+ * - login/challenge-state check.
13
+ * `ensureSiteAuthenticated` runs them against snapshot text and throws
14
+ * BrowserAuthRequired when a login/challenge is detected.
15
+ */
16
+ export interface SiteAuthVerifier {
17
+ /** Stable provider id used in BrowserAuthRequired + status UX. */
18
+ provider: string;
19
+ /** Page to open for the auth check. */
20
+ destinationUrl: string;
21
+ /** True when the snapshot shows the authenticated state. */
22
+ isAuthenticated: (snapshotText: string, url: string) => boolean;
23
+ /** True when the snapshot shows a login/challenge wall. */
24
+ isLoginOrChallenge: (snapshotText: string, url: string) => boolean;
25
+ }
26
+ export interface AuthCheckPage {
27
+ url: string;
28
+ snapshotText: string;
29
+ }
30
+ export type AuthCheckResult = {
31
+ status: 'authenticated';
32
+ } | {
33
+ status: 'auth-required';
34
+ provider: string;
35
+ url: string;
36
+ } | {
37
+ status: 'unknown';
38
+ };
39
+ /** Classify one snapshot without side effects. */
40
+ export declare function checkSiteAuth(verifier: SiteAuthVerifier, page: AuthCheckPage): AuthCheckResult;
41
+ /** Gmail: inbox DOM vs Google login/challenge (never conflate Chrome-profile
42
+ * sign-in with a mail.google.com session — they are different states). */
43
+ export declare const gmailVerifier: SiteAuthVerifier;
44
+ /** GitHub: settings/profile page shows the username when signed in. */
45
+ export declare const githubVerifier: SiteAuthVerifier;
46
+ /** Registry of built-in verifiers, keyed by provider id. */
47
+ export declare const BUILTIN_VERIFIERS: Record<string, SiteAuthVerifier>;
48
+ export interface AuthBrowser {
49
+ openPage(url: string): Promise<{
50
+ url: string;
51
+ snapshotText: string;
52
+ }>;
53
+ }
54
+ /**
55
+ * Open the verifier destination, classify, and either return or throw
56
+ * BrowserAuthRequired. `unknown` is returned (not thrown) so callers can
57
+ * decide — an unrecognized page is not proof of a login wall.
58
+ */
59
+ export declare function ensureSiteAuthenticated(browser: AuthBrowser, verifier: SiteAuthVerifier): Promise<AuthCheckResult>;
60
+ //# sourceMappingURL=auth-verifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-verifiers.d.ts","sourceRoot":"","sources":["../src/auth-verifiers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,uCAAuC;IACvC,cAAc,EAAE,MAAM,CAAA;IACtB,4DAA4D;IAC5D,eAAe,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/D,2DAA2D;IAC3D,kBAAkB,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CACnE;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,MAAM,EAAE,eAAe,CAAA;CAAE,GAC3B;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAA;AAEzB,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,CAM9F;AAwBD;0EAC0E;AAC1E,eAAO,MAAM,aAAa,EAAE,gBAc3B,CAAA;AAID,uEAAuE;AACvE,eAAO,MAAM,cAAc,EAAE,gBAU5B,CAAA;AAED,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAG9D,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACtE;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CAO1B"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Skill-level site authentication verifiers (spec section 6).
3
+ *
4
+ * There is intentionally no generic `isAuthenticated()` heuristic: each site
5
+ * presents different authenticated and unauthenticated states, so detection
6
+ * lives here per site. The browser subsystem only handles the resulting
7
+ * BrowserAuthRequired by switching Persistent into reauthentication.
8
+ *
9
+ * A verifier declares:
10
+ * - destination URL (where to check),
11
+ * - authenticated-state check,
12
+ * - login/challenge-state check.
13
+ * `ensureSiteAuthenticated` runs them against snapshot text and throws
14
+ * BrowserAuthRequired when a login/challenge is detected.
15
+ */
16
+ import { BrowserAuthRequired } from './session.js';
17
+ /** Classify one snapshot without side effects. */
18
+ export function checkSiteAuth(verifier, page) {
19
+ if (verifier.isAuthenticated(page.snapshotText, page.url))
20
+ return { status: 'authenticated' };
21
+ if (verifier.isLoginOrChallenge(page.snapshotText, page.url)) {
22
+ return { status: 'auth-required', provider: verifier.provider, url: page.url };
23
+ }
24
+ return { status: 'unknown' };
25
+ }
26
+ const GOOGLE_LOGIN_URL = /accounts\.google\.com\/(v\d+\/)?(signin|ServiceLogin|challenge|password|otp|verification)/i;
27
+ const GOOGLE_LOGIN_QUERY = /[?&](flowEntry=ServiceLogin|flowName=WebLiteSignIn|service=mail)/i;
28
+ const GOOGLE_CHALLENGE_COPY = /verify it'?s you|2-step verification|two-factor|enter your password|choose an account to continue|sign in to continue to gmail|to continue to gmail/i;
29
+ /** Raw-DOM login redirect: Gmail serves a JS hop to accounts.google.com. */
30
+ const GMAIL_LOGIN_BASE = /<base\s+href="https:\/\/accounts\.google\.com/i;
31
+ /**
32
+ * Inbox markers. Deliberately excludes bare "primary": Google login pages
33
+ * embed it in CSS custom properties (--gm3-sys-color-primary), which caused
34
+ * a false authenticated verdict on real sign-in HTML (live-tested 2026-09).
35
+ */
36
+ const GMAIL_INBOX_MARKERS = [
37
+ /inbox/i,
38
+ /compose/i,
39
+ /search mail/i,
40
+ /conversation view/i,
41
+ /sent mail/i,
42
+ /\bdrafts\b/i,
43
+ ];
44
+ /** Gmail: inbox DOM vs Google login/challenge (never conflate Chrome-profile
45
+ * sign-in with a mail.google.com session — they are different states). */
46
+ export const gmailVerifier = {
47
+ provider: 'google',
48
+ destinationUrl: 'https://mail.google.com/',
49
+ isAuthenticated: (text, url) => {
50
+ if (!/mail\.google\.com/i.test(url))
51
+ return false;
52
+ if (GOOGLE_LOGIN_URL.test(url) || GOOGLE_LOGIN_QUERY.test(url))
53
+ return false;
54
+ if (GMAIL_LOGIN_BASE.test(text) || GOOGLE_CHALLENGE_COPY.test(text))
55
+ return false;
56
+ return GMAIL_INBOX_MARKERS.filter((pattern) => pattern.test(text)).length >= 2;
57
+ },
58
+ isLoginOrChallenge: (text, url) => GOOGLE_LOGIN_URL.test(url) ||
59
+ GOOGLE_LOGIN_QUERY.test(url) ||
60
+ GMAIL_LOGIN_BASE.test(text) ||
61
+ GOOGLE_CHALLENGE_COPY.test(text),
62
+ };
63
+ const GITHUB_LOGIN_URL = /(^|\/)((login|session|auth)(\/|$|[?#]))/i;
64
+ /** GitHub: settings/profile page shows the username when signed in. */
65
+ export const githubVerifier = {
66
+ provider: 'github',
67
+ destinationUrl: 'https://github.com/settings/profile',
68
+ isAuthenticated: (text, url) => /github\.com\/settings\/profile/i.test(url) &&
69
+ /public profile|update profile|contributions|profile picture/i.test(text),
70
+ isLoginOrChallenge: (text, url) => /github\.com\/login/i.test(url) ||
71
+ GITHUB_LOGIN_URL.test(url) ||
72
+ (/sign in to github/i.test(text) && /username or email|password/i.test(text)),
73
+ };
74
+ /** Registry of built-in verifiers, keyed by provider id. */
75
+ export const BUILTIN_VERIFIERS = {
76
+ google: gmailVerifier,
77
+ github: githubVerifier,
78
+ };
79
+ /**
80
+ * Open the verifier destination, classify, and either return or throw
81
+ * BrowserAuthRequired. `unknown` is returned (not thrown) so callers can
82
+ * decide — an unrecognized page is not proof of a login wall.
83
+ */
84
+ export async function ensureSiteAuthenticated(browser, verifier) {
85
+ const page = await browser.openPage(verifier.destinationUrl);
86
+ const result = checkSiteAuth(verifier, page);
87
+ if (result.status === 'auth-required') {
88
+ throw new BrowserAuthRequired({ provider: result.provider, url: result.url });
89
+ }
90
+ return result;
91
+ }
92
+ //# sourceMappingURL=auth-verifiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-verifiers.js","sourceRoot":"","sources":["../src/auth-verifiers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Pi-owned Chrome process management (spec sections 3 and 4).
3
+ *
4
+ * Normal automation launches Chrome directly with the Pi profile and an
5
+ * ephemeral loopback remote-debugging port, then MCP attaches via
6
+ * `--browser-url`. This keeps one Chrome process per profile and avoids the
7
+ * default WebDriver launch path where authentication often breaks:
8
+ *
9
+ * ```text
10
+ * Google Chrome --user-data-dir="<pi-profile>"
11
+ * --remote-debugging-port=<ephemeral> [--headless]
12
+ * ↕
13
+ * chrome-devtools-mcp --browser-url=http://127.0.0.1:<port>
14
+ * ```
15
+ *
16
+ * Bootstrap (first-run auth) launches headed Chrome *without* MCP/Puppeteer
17
+ * so it looks like an ordinary manually launched browser.
18
+ */
19
+ export interface ChromeLaunchOptions {
20
+ userDataDir: string;
21
+ /** Named profile directory inside userDataDir (e.g. pi-browser-use). */
22
+ profileDirectory?: string;
23
+ /** Ephemeral loopback port. Allocated automatically when omitted. */
24
+ port?: number;
25
+ headless?: boolean;
26
+ /** Extra Chrome flags appended after the managed ones. */
27
+ chromeArgs?: string[];
28
+ executablePath?: string;
29
+ /** How long to wait for the DevTools endpoint. Default 15s. */
30
+ readyTimeoutMs?: number;
31
+ }
32
+ export interface ChromeProcess {
33
+ readonly pid: number | undefined;
34
+ readonly port: number;
35
+ readonly browserUrl: string;
36
+ readonly userDataDir: string;
37
+ /** True once the process has exited. */
38
+ readonly exited: boolean;
39
+ /** Resolves when the process exits (bootstrap uses this: close → READY). */
40
+ waitForExit(): Promise<number | null>;
41
+ /** SIGTERM, then SIGKILL after `graceMs` if still alive. */
42
+ shutdown(graceMs?: number): Promise<void>;
43
+ }
44
+ /** Chrome/Chromium executable candidates by platform (stable first). */
45
+ export declare function chromeExecutableCandidates(): string[];
46
+ /** Resolve the Chrome executable: explicit path wins, else first candidate. */
47
+ export declare function findChromeExecutable(executablePath?: string): string;
48
+ /** Allocate a free loopback port (never hardcode 9222). */
49
+ export declare function allocateEphemeralPort(): Promise<number>;
50
+ /**
51
+ * Managed Chrome flags. Headed bootstrap omits `--headless` and debugging
52
+ * entirely when `debugPort` is 0 (plain manual browser, spec section 3).
53
+ */
54
+ export declare function buildChromeArgs(options: {
55
+ userDataDir: string;
56
+ profileDirectory?: string;
57
+ port?: number;
58
+ headless?: boolean;
59
+ chromeArgs?: string[];
60
+ }): string[];
61
+ /** Poll the DevTools `/json/version` endpoint until it answers or times out. */
62
+ export declare function waitForDevToolsEndpoint(port: number, options?: {
63
+ timeoutMs?: number;
64
+ fetchImpl?: typeof fetch;
65
+ }): Promise<{
66
+ browserUrl: string;
67
+ webSocketDebuggerUrl: string;
68
+ }>;
69
+ /**
70
+ * Launch Pi-owned Chrome. The caller must hold the profile lock
71
+ * (see `profile-lock.ts`) for `userDataDir` before calling.
72
+ */
73
+ export declare function launchChrome(options: ChromeLaunchOptions): Promise<ChromeProcess>;
74
+ /**
75
+ * AppleScript that raises the exact Chrome process by pid. Unlike app-level
76
+ * `activate` (which may front the user's daily windows instead), this targets
77
+ * Pi-owned Chrome only. Foreground is reserved for explicit user-requested
78
+ * views and auth handoffs — never automation.
79
+ */
80
+ export declare function buildFrontProcessScript(pid: number): string;
81
+ /** Best-effort fronting of Pi-owned Chrome (macOS only). Returns success. */
82
+ export declare function frontProcessByPid(pid: number | undefined, runner?: (cmd: string, args: string[]) => void): boolean;
83
+ /**
84
+ * AppleScript that raises the Chrome window holding the marker URL.
85
+ * Pure (testable): execution lives with the caller. Foreground is reserved
86
+ * for explicit user-requested views and auth handoffs — never automation.
87
+ */
88
+ export declare function buildFocusWindowScript(markerUrl: string): string;
89
+ /**
90
+ * Launch the headed first-run/setup browser (spec section 3): same Pi
91
+ * profile, no MCP, no Puppeteer, no remote debugging — an ordinary manually
92
+ * launched Chrome. Resolves when the user closes the window.
93
+ */
94
+ export declare function launchSetupBrowser(options: {
95
+ userDataDir: string;
96
+ profileDirectory?: string;
97
+ executablePath?: string;
98
+ chromeArgs?: string[];
99
+ }): Promise<number | null>;
100
+ //# sourceMappingURL=chrome-launcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chrome-launcher.d.ts","sourceRoot":"","sources":["../src/chrome-launcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,4EAA4E;IAC5E,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1C;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,IAAI,MAAM,EAAE,CAsBrD;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAepE;AAED,2DAA2D;AAC3D,wBAAgB,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC,CAavD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB,GAAG,MAAM,EAAE,CAcX;AAED,gFAAgF;AAChF,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAAE,GACzD,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,oBAAoB,EAAE,MAAM,CAAA;CAAE,CAAC,CAsB/D;AAgDD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CA8BvF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,GAC7C,OAAO,CAST;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAehE;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYzB"}
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Pi-owned Chrome process management (spec sections 3 and 4).
3
+ *
4
+ * Normal automation launches Chrome directly with the Pi profile and an
5
+ * ephemeral loopback remote-debugging port, then MCP attaches via
6
+ * `--browser-url`. This keeps one Chrome process per profile and avoids the
7
+ * default WebDriver launch path where authentication often breaks:
8
+ *
9
+ * ```text
10
+ * Google Chrome --user-data-dir="<pi-profile>"
11
+ * --remote-debugging-port=<ephemeral> [--headless]
12
+ * ↕
13
+ * chrome-devtools-mcp --browser-url=http://127.0.0.1:<port>
14
+ * ```
15
+ *
16
+ * Bootstrap (first-run auth) launches headed Chrome *without* MCP/Puppeteer
17
+ * so it looks like an ordinary manually launched browser.
18
+ */
19
+ import { execFileSync, spawn } from 'node:child_process';
20
+ import { existsSync } from 'node:fs';
21
+ import { createServer } from 'node:net';
22
+ /** Chrome/Chromium executable candidates by platform (stable first). */
23
+ export function chromeExecutableCandidates() {
24
+ if (process.platform === 'darwin') {
25
+ return [
26
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
27
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
28
+ '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
29
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
30
+ ];
31
+ }
32
+ if (process.platform === 'win32') {
33
+ return [
34
+ `${process.env['PROGRAMFILES'] ?? 'C:\\Program Files'}\\Google\\Chrome\\Application\\chrome.exe`,
35
+ `${process.env['PROGRAMFILES(X86)'] ?? 'C:\\Program Files (x86)'}\\Google\\Chrome\\Application\\chrome.exe`,
36
+ ];
37
+ }
38
+ return [
39
+ '/usr/bin/google-chrome-stable',
40
+ '/usr/bin/google-chrome',
41
+ '/usr/bin/chromium',
42
+ '/usr/bin/chromium-browser',
43
+ '/snap/bin/chromium',
44
+ ];
45
+ }
46
+ /** Resolve the Chrome executable: explicit path wins, else first candidate. */
47
+ export function findChromeExecutable(executablePath) {
48
+ if (executablePath && executablePath.length > 0) {
49
+ if (!existsSync(executablePath)) {
50
+ throw new Error(`Chrome executable not found: ${executablePath}`);
51
+ }
52
+ return executablePath;
53
+ }
54
+ const envPath = process.env['CHROME_PATH']?.trim();
55
+ if (envPath && existsSync(envPath))
56
+ return envPath;
57
+ for (const candidate of chromeExecutableCandidates()) {
58
+ if (existsSync(candidate))
59
+ return candidate;
60
+ }
61
+ throw new Error('Chrome executable was not found. Install Google Chrome Stable or set executablePath.');
62
+ }
63
+ /** Allocate a free loopback port (never hardcode 9222). */
64
+ export function allocateEphemeralPort() {
65
+ return new Promise((resolve, reject) => {
66
+ const server = createServer();
67
+ server.on('error', reject);
68
+ server.listen(0, '127.0.0.1', () => {
69
+ const address = server.address();
70
+ const port = typeof address === 'object' && address ? address.port : 0;
71
+ server.close((error) => {
72
+ if (error)
73
+ reject(error);
74
+ else
75
+ resolve(port);
76
+ });
77
+ });
78
+ });
79
+ }
80
+ /**
81
+ * Managed Chrome flags. Headed bootstrap omits `--headless` and debugging
82
+ * entirely when `debugPort` is 0 (plain manual browser, spec section 3).
83
+ */
84
+ export function buildChromeArgs(options) {
85
+ const args = [
86
+ `--user-data-dir=${options.userDataDir}`,
87
+ '--no-first-run',
88
+ '--no-default-browser-check',
89
+ ];
90
+ if (options.profileDirectory)
91
+ args.push(`--profile-directory=${options.profileDirectory}`);
92
+ if (options.port !== undefined && options.port > 0) {
93
+ args.push(`--remote-debugging-port=${options.port}`);
94
+ args.push('--remote-allow-origins=*');
95
+ }
96
+ if (options.headless === true)
97
+ args.push('--headless');
98
+ args.push(...(options.chromeArgs ?? []));
99
+ return args;
100
+ }
101
+ /** Poll the DevTools `/json/version` endpoint until it answers or times out. */
102
+ export async function waitForDevToolsEndpoint(port, options) {
103
+ const timeoutMs = options?.timeoutMs ?? 15_000;
104
+ const fetchImpl = options?.fetchImpl ?? fetch;
105
+ const browserUrl = `http://127.0.0.1:${port}`;
106
+ const deadline = Date.now() + timeoutMs;
107
+ let lastError;
108
+ while (Date.now() < deadline) {
109
+ try {
110
+ const response = await fetchImpl(`${browserUrl}/json/version`);
111
+ if (response.ok) {
112
+ const info = (await response.json());
113
+ return { browserUrl, webSocketDebuggerUrl: info.webSocketDebuggerUrl ?? '' };
114
+ }
115
+ lastError = new Error(`DevTools endpoint answered ${response.status}`);
116
+ }
117
+ catch (error) {
118
+ lastError = error;
119
+ }
120
+ await new Promise((resolve) => setTimeout(resolve, 100));
121
+ }
122
+ throw new Error(`Timed out waiting for Chrome DevTools on ${browserUrl} (${lastError instanceof Error ? lastError.message : String(lastError)})`);
123
+ }
124
+ class OwnedChromeProcess {
125
+ port;
126
+ browserUrl;
127
+ userDataDir;
128
+ child;
129
+ exitPromise;
130
+ exitedFlag = false;
131
+ constructor(child, port, browserUrl, userDataDir) {
132
+ this.port = port;
133
+ this.browserUrl = browserUrl;
134
+ this.userDataDir = userDataDir;
135
+ this.child = child;
136
+ this.exitPromise = new Promise((resolve) => {
137
+ child.on('exit', (code) => {
138
+ this.exitedFlag = true;
139
+ resolve(code);
140
+ });
141
+ });
142
+ }
143
+ get pid() {
144
+ return this.child.pid;
145
+ }
146
+ get exited() {
147
+ return this.exitedFlag || this.child.exitCode !== null;
148
+ }
149
+ waitForExit() {
150
+ return this.exitPromise;
151
+ }
152
+ async shutdown(graceMs = 3_000) {
153
+ if (this.exited)
154
+ return;
155
+ this.child.kill('SIGTERM');
156
+ const exited = await Promise.race([
157
+ this.exitPromise.then(() => true),
158
+ new Promise((resolve) => setTimeout(() => resolve(false), graceMs)),
159
+ ]);
160
+ if (!exited && !this.exited) {
161
+ this.child.kill('SIGKILL');
162
+ await this.exitPromise;
163
+ }
164
+ }
165
+ }
166
+ /**
167
+ * Launch Pi-owned Chrome. The caller must hold the profile lock
168
+ * (see `profile-lock.ts`) for `userDataDir` before calling.
169
+ */
170
+ export async function launchChrome(options) {
171
+ const executable = findChromeExecutable(options.executablePath);
172
+ const port = options.port ?? (await allocateEphemeralPort());
173
+ const args = buildChromeArgs({
174
+ userDataDir: options.userDataDir,
175
+ profileDirectory: options.profileDirectory,
176
+ port,
177
+ headless: options.headless,
178
+ chromeArgs: options.chromeArgs,
179
+ });
180
+ const child = spawn(executable, args, { stdio: 'ignore', detached: false });
181
+ await new Promise((resolve, reject) => {
182
+ child.on('error', reject);
183
+ // Give spawn a tick to surface ENOENT-style failures before probing.
184
+ setTimeout(resolve, 50);
185
+ });
186
+ if (child.exitCode !== null) {
187
+ throw new Error(`Chrome exited immediately (code ${child.exitCode}).`);
188
+ }
189
+ try {
190
+ await waitForDevToolsEndpoint(port, { timeoutMs: options.readyTimeoutMs });
191
+ }
192
+ catch (error) {
193
+ try {
194
+ child.kill('SIGKILL');
195
+ }
196
+ catch {
197
+ // Already gone; the endpoint error below is what matters.
198
+ }
199
+ throw error;
200
+ }
201
+ return new OwnedChromeProcess(child, port, `http://127.0.0.1:${port}`, options.userDataDir);
202
+ }
203
+ /**
204
+ * AppleScript that raises the exact Chrome process by pid. Unlike app-level
205
+ * `activate` (which may front the user's daily windows instead), this targets
206
+ * Pi-owned Chrome only. Foreground is reserved for explicit user-requested
207
+ * views and auth handoffs — never automation.
208
+ */
209
+ export function buildFrontProcessScript(pid) {
210
+ return [
211
+ 'tell application "System Events"',
212
+ `set frontmost of (first application process whose unix id is ${pid}) to true`,
213
+ 'end tell',
214
+ ].join('\n');
215
+ }
216
+ /** Best-effort fronting of Pi-owned Chrome (macOS only). Returns success. */
217
+ export function frontProcessByPid(pid, runner) {
218
+ if (pid === undefined || process.platform !== 'darwin')
219
+ return false;
220
+ try {
221
+ const run = runner ?? ((cmd, args) => void execFileSync(cmd, args));
222
+ run('osascript', ['-e', buildFrontProcessScript(pid)]);
223
+ return true;
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ /**
230
+ * AppleScript that raises the Chrome window holding the marker URL.
231
+ * Pure (testable): execution lives with the caller. Foreground is reserved
232
+ * for explicit user-requested views and auth handoffs — never automation.
233
+ */
234
+ export function buildFocusWindowScript(markerUrl) {
235
+ const needle = markerUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
236
+ return [
237
+ 'tell application "Google Chrome"',
238
+ 'activate',
239
+ 'repeat with w in every window',
240
+ 'repeat with t in every tab of w',
241
+ `if (URL of t starts with "${needle}") then`,
242
+ 'set index of w to 1',
243
+ 'exit repeat',
244
+ 'end if',
245
+ 'end repeat',
246
+ 'end repeat',
247
+ 'end tell',
248
+ ].join('\n');
249
+ }
250
+ /**
251
+ * Launch the headed first-run/setup browser (spec section 3): same Pi
252
+ * profile, no MCP, no Puppeteer, no remote debugging — an ordinary manually
253
+ * launched Chrome. Resolves when the user closes the window.
254
+ */
255
+ export async function launchSetupBrowser(options) {
256
+ const executable = findChromeExecutable(options.executablePath);
257
+ const args = buildChromeArgs({
258
+ userDataDir: options.userDataDir,
259
+ profileDirectory: options.profileDirectory,
260
+ chromeArgs: options.chromeArgs,
261
+ });
262
+ const child = spawn(executable, args, { stdio: 'ignore', detached: false });
263
+ return new Promise((resolve, reject) => {
264
+ child.on('error', reject);
265
+ child.on('exit', (code) => resolve(code));
266
+ });
267
+ }
268
+ //# sourceMappingURL=chrome-launcher.js.map