Skip to content

Commit bdc4b8a

Browse files
committed
chore: restore state files after revert
Made-with: Cursor
1 parent e77e0b5 commit bdc4b8a

3 files changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Reddit Adapter Phase 2: Authenticated Features
2+
3+
## Prerequisites
4+
5+
- Phase 1 complete: `browserkit-dev/adapter-reddit` published with 4 public tools (`get_subreddit`, `get_thread`, `search`, `get_user`)
6+
- CI passing on Phase 1
7+
8+
## 2a. Auth detection
9+
10+
Replace `isLoggedIn: () => true` with real detection in `src/index.ts`:
11+
12+
```typescript
13+
async isLoggedIn(page: Page): Promise<boolean> {
14+
// old.reddit.com shows username link in header when logged in
15+
const userLink = await page.locator('#header-bottom-right .user a').count();
16+
if (userLink > 0) return true;
17+
// Fallback: check for login prompt text
18+
const loginPrompt = await page.locator('#header-bottom-right .login-required').count();
19+
return loginPrompt === 0;
20+
}
21+
```
22+
23+
## 2b. New tools (3)
24+
25+
| Tool | Inputs | URL pattern | Description |
26+
|------|--------|-------------|-------------|
27+
| `get_feed` | `sort?` (best/hot/new/rising/top), `count?` (1-50) | `old.reddit.com/` (logged-in front page) | Personal front page based on subscriptions |
28+
| `get_saved` | `count?` (1-50) | `old.reddit.com/user/{me}/saved` | Saved posts and comments |
29+
| `get_messages` | `section?` (inbox/unread/sent), `count?` (1-25) | `old.reddit.com/message/{section}` | Reddit inbox messages |
30+
31+
Phase 1 tools continue to work for both authenticated and unauthenticated users. Phase 2 tools return handoff errors when not logged in (framework handles this automatically via `isLoggedIn` check before each tool call).
32+
33+
`get_feed` reuses `scrapePostListing` from Phase 1. `get_saved` reuses the same scraper but targets the saved page. `get_messages` needs a new `scrapeMessages` function in `scraper.ts` for the message DOM.
34+
35+
## 2c. Config
36+
37+
```javascript
38+
adapters: {
39+
"@browserkit/adapter-reddit": { port: 3849 },
40+
// no channel: "chrome" needed — Reddit doesn't block Chromium login
41+
}
42+
```
43+
44+
Login: `browserkit login reddit` opens old.reddit.com/login in a headed browser.
45+
46+
## 2d. Testing
47+
48+
**Unit tests (update `reddit.test.ts`):**
49+
- 3 new tool schemas validated
50+
- `isLoggedIn` with mocked page (logged in vs logged out HTML)
51+
- `scrapeMessages` pure function tests
52+
53+
**L3 MCP protocol (update `mcp-protocol.test.ts`):**
54+
- Tool count increases from 6 to 9
55+
- `get_feed` / `get_saved` / `get_messages` dispatch correctly
56+
- Unauthenticated calls to auth tools return handoff error
57+
58+
**L2 integration (`reddit.integration.test.ts`):**
59+
- `get_feed` returns personalized posts (requires `browserkit login reddit`)
60+
- `get_saved` returns saved items
61+
- `get_messages` returns inbox
62+
- Mark these as skip-in-CI (need live auth session)
63+
64+
## 2e. Execution order
65+
66+
1. Update `isLoggedIn` with real auth detection
67+
2. Add `scrapeMessages` to `scraper.ts`
68+
3. Add `get_feed`, `get_saved`, `get_messages` tool handlers to `index.ts`
69+
4. Update L1 + L3 tests for new tools
70+
5. Add L2 auth integration tests (excluded from CI)
71+
6. Push, verify CI passes
72+
7. Update main browserkit README if needed

packages/core/src/human-handoff.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,56 @@ function raceWithTimeout(promise: Promise<boolean>, timeoutMs: number): Promise<
208208
function sleep(ms: number): Promise<void> {
209209
return new Promise((resolve) => setTimeout(resolve, ms));
210210
}
211+
212+
// ── loginViaConnect ───────────────────────────────────────────────────────────
213+
214+
/**
215+
* Attaches to an already-running Chrome instance via CDP, checks whether the
216+
* adapter reports a logged-in state, and — if so — saves the storageState to
217+
* disk so `SessionManager` can pick it up on next start.
218+
*
219+
* Use this with `browserkit login --connect` to avoid opening a second browser
220+
* when the user already has Chrome running and logged in.
221+
*/
222+
export async function loginViaConnect(
223+
sessionManager: Pick<SessionManager, "getProfileDir" | "injectStorageState">,
224+
config: Pick<SessionConfig, "site" | "domain" | "authStrategy" | "profileDir">,
225+
adapter: Pick<SiteAdapter, "isLoggedIn">,
226+
cdpEndpoint: string
227+
): Promise<{ outcome: "success" | "timeout"; durationMs: number }> {
228+
const start = Date.now();
229+
230+
const browser = await chromium.connectOverCDP(cdpEndpoint);
231+
try {
232+
const context = browser.contexts()[0];
233+
if (!context) return { outcome: "timeout", durationMs: Date.now() - start };
234+
235+
const pages = context.pages();
236+
const page = pages[0];
237+
if (!page) return { outcome: "timeout", durationMs: Date.now() - start };
238+
239+
const loggedIn = await adapter.isLoggedIn(page as never);
240+
if (!loggedIn) return { outcome: "timeout", durationMs: Date.now() - start };
241+
242+
// Logged in — persist the storageState
243+
const state = await context.storageState();
244+
if (config.authStrategy === "storage-state") {
245+
const profileDir = sessionManager.getProfileDir(config.site ?? config.profileDir);
246+
fs.mkdirSync(profileDir, { recursive: true });
247+
fs.writeFileSync(
248+
path.join(profileDir, "storage-state.json"),
249+
JSON.stringify(state, null, 2)
250+
);
251+
} else {
252+
await sessionManager.injectStorageState(
253+
config.site,
254+
state.cookies as never,
255+
state.origins as never
256+
);
257+
}
258+
259+
return { outcome: "success", durationMs: Date.now() - start };
260+
} finally {
261+
await browser.close();
262+
}
263+
}

packages/core/src/session-manager.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,70 @@ export function getDefaultDataDir(): string {
382382
function isProcessRunning(pid: number): boolean {
383383
try { process.kill(pid, 0); return true; } catch { return false; }
384384
}
385+
386+
// ── autoDiscoverCdpEndpoint ───────────────────────────────────────────────────
387+
388+
/**
389+
* Scans well-known Chrome user-data directories for a `DevToolsActivePort`
390+
* file written by a running Chrome process (requires `--remote-debugging-port`).
391+
*
392+
* Returns the first valid CDP WebSocket URL found, e.g.
393+
* `ws://127.0.0.1:9222/devtools/browser/<id>`
394+
*
395+
* Throws if no running Chrome instance is discovered.
396+
*/
397+
export async function autoDiscoverCdpEndpoint(expectedPort?: number): Promise<string> {
398+
const candidates: string[] = [];
399+
400+
switch (process.platform) {
401+
case "darwin":
402+
candidates.push(
403+
path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome"),
404+
path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome Beta"),
405+
path.join(os.homedir(), "Library", "Application Support", "Chromium"),
406+
path.join(os.homedir(), "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
407+
);
408+
break;
409+
case "linux":
410+
candidates.push(
411+
path.join(os.homedir(), ".config", "google-chrome"),
412+
path.join(os.homedir(), ".config", "chromium"),
413+
);
414+
break;
415+
case "win32":
416+
candidates.push(
417+
path.join(os.homedir(), "AppData", "Local", "Google", "Chrome", "User Data"),
418+
path.join(os.homedir(), "AppData", "Local", "Chromium", "User Data"),
419+
);
420+
break;
421+
}
422+
423+
for (const dir of candidates) {
424+
// DevToolsActivePort can appear in the root or inside a profile subfolder
425+
for (const sub of ["", "Default"]) {
426+
const file = path.join(dir, sub, "DevToolsActivePort");
427+
try {
428+
const contents = fs.readFileSync(file, "utf8");
429+
const ws = parseDevToolsActivePort(contents, expectedPort);
430+
if (ws) return ws;
431+
} catch {
432+
// file not found or unreadable — keep scanning
433+
}
434+
}
435+
}
436+
437+
throw new Error(
438+
"Could not auto-discover a running Chrome instance with remote debugging enabled. " +
439+
"Launch Chrome with --remote-debugging-port=<port> first, or pass the CDP URL explicitly."
440+
);
441+
}
442+
443+
function parseDevToolsActivePort(contents: string, expectedPort?: number): string | null {
444+
const lines = contents.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
445+
const port = Number.parseInt(lines[0] ?? "", 10);
446+
const wsPath = lines[1] ?? "";
447+
if (!Number.isInteger(port) || port < 1 || port > 65_535) return null;
448+
if (expectedPort !== undefined && port !== expectedPort) return null;
449+
if (!wsPath.startsWith("/devtools/browser/")) return null;
450+
return `ws://127.0.0.1:${port}${wsPath}`;
451+
}

0 commit comments

Comments
 (0)