libretto 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/scripts/postinstall.mjs +48 -0
- package/skill/SKILL.md +438 -0
- package/skill/code-generation-rules.md +190 -0
- package/skill/integration-approach-selection.md +174 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
-
"bin"
|
|
16
|
+
"bin",
|
|
17
|
+
"scripts",
|
|
18
|
+
"skill"
|
|
17
19
|
],
|
|
18
20
|
"bin": {
|
|
19
21
|
"libretto": "./bin/libretto.mjs",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
function isDirectory(path) {
|
|
6
|
+
if (!existsSync(path)) return false;
|
|
7
|
+
return lstatSync(path).isDirectory();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function log(message) {
|
|
11
|
+
console.log(`[libretto postinstall] ${message}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function main() {
|
|
15
|
+
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
const initCwd = process.env.INIT_CWD ? resolve(process.env.INIT_CWD) : null;
|
|
17
|
+
const installRoot = initCwd ?? process.cwd();
|
|
18
|
+
|
|
19
|
+
const sourceSkillDir = join(packageDir, "skill");
|
|
20
|
+
if (!isDirectory(sourceSkillDir)) {
|
|
21
|
+
log(`Skipped: source skill directory not found at "${sourceSkillDir}".`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const targets = [
|
|
26
|
+
join(installRoot, ".agents", "skills"),
|
|
27
|
+
join(installRoot, ".claude", "skills"),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const skillsRoot of targets) {
|
|
31
|
+
if (!isDirectory(skillsRoot)) {
|
|
32
|
+
log(`Skipped: "${skillsRoot}" does not exist.`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const destinationSkillDir = join(skillsRoot, "libretto");
|
|
37
|
+
mkdirSync(destinationSkillDir, { recursive: true });
|
|
38
|
+
cpSync(sourceSkillDir, destinationSkillDir, { recursive: true, force: true });
|
|
39
|
+
log(`Synced skill "libretto" to "${skillsRoot}/libretto".`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
main();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
console.warn(`[libretto postinstall] Warning: ${message}`);
|
|
48
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: libretto
|
|
3
|
+
description: "Browser automation CLI for building integrations, with a network-first approach.\n\nWHEN TO USE THIS SKILL:\n- When building a new integration or data extraction workflow against a website\n- When you need to interact with a web page (click, fill, navigate) rather than just read it\n- When debugging browser agent job failures (selectors timing out, clicks not working, elements not found)\n- When you need to test or prototype Playwright interactions before codifying them\n- When you need to save or restore login sessions for authenticated pages\n- When you need to understand what's on a page (use the snapshot command)\n- When scraping dynamic content that requires JavaScript execution\n\nWHEN NOT TO USE THIS SKILL:\n- When you only need to read static web content (use read_web_page instead)\n- When you need to modify browser agent source code (edit files directly)\n- When you need to run a full browser agent job end-to-end (use npx browser-agent CLI)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Browser Integration with Libretto CLI
|
|
7
|
+
|
|
8
|
+
Use the `npx libretto` CLI to automate web interactions, debug browser agent jobs, and prototype fixes.
|
|
9
|
+
|
|
10
|
+
## CRITICAL: Session Access
|
|
11
|
+
|
|
12
|
+
Libretto sessions are **full-access by default**. You can use `exec` and `run` immediately after opening a session.
|
|
13
|
+
|
|
14
|
+
**Rules:**
|
|
15
|
+
- Always announce which session you opened and what page you are on.
|
|
16
|
+
- Use `snapshot`, `network`, and `actions` first when debugging unknown page state.
|
|
17
|
+
- Before any potentially mutating action (submit/save/delete, or non-idempotent API calls), describe what you are about to do and wait for explicit user confirmation.
|
|
18
|
+
|
|
19
|
+
## Ask, Don't Guess
|
|
20
|
+
|
|
21
|
+
If it's not obvious which element to click or what value to enter, **ask the user** — don't try multiple things hoping one works. Present what you see on the page and let the user tell you where to go. One question is faster than a 30-second timeout from a wrong guess.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx libretto open <url> [--headless] # Launch browser and navigate (headed by default)
|
|
27
|
+
npx libretto exec <code> [--visualize] # Execute Playwright TypeScript code (--visualize enables ghost cursor + highlight)
|
|
28
|
+
npx libretto run <integrationFile> <integrationExport> # Execute integration actions
|
|
29
|
+
npx libretto resume # Resume a paused workflow for the current session
|
|
30
|
+
npx libretto snapshot --objective "<what to find>" [--context "<situational info>"]
|
|
31
|
+
npx libretto save <url|domain> # Save session (cookies, localStorage) to .libretto/profiles/
|
|
32
|
+
npx libretto network # Show last 20 captured network requests
|
|
33
|
+
npx libretto actions # Show last 20 captured user/agent actions
|
|
34
|
+
npx libretto close # Close the browser
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
All commands accept `--session <name>` for isolated browser instances (default: `default`).
|
|
38
|
+
Built-in sessions: `default`, `dev-server`, `browser-agent`.
|
|
39
|
+
|
|
40
|
+
## Visualize Mode (`--visualize`)
|
|
41
|
+
|
|
42
|
+
Add `--visualize` to any `exec` command to show a ghost cursor and element highlight before each action executes. Use it when the user wants to see what will be clicked/filled before it happens.
|
|
43
|
+
|
|
44
|
+
## Workflow Pause/Resume (`ctx.pause()`)
|
|
45
|
+
|
|
46
|
+
Workflows pause from inside the workflow function by calling `await ctx.pause()`.
|
|
47
|
+
|
|
48
|
+
- There are no pause options to pass at call sites. Pause is session-scoped and resolved from the active session.
|
|
49
|
+
- `npx libretto run ...` waits until the workflow either completes or hits the next `ctx.pause()`.
|
|
50
|
+
- On pause, the workflow process stays alive and keeps browser/session state.
|
|
51
|
+
- `npx libretto resume --session <name>` sends resume signal and then waits until completion or the next pause.
|
|
52
|
+
- For multi-pause workflows, call `resume` repeatedly until the workflow completes.
|
|
53
|
+
|
|
54
|
+
## Globals Available in `exec`
|
|
55
|
+
|
|
56
|
+
`page`, `context`, `state`, `browser`, `networkLog({ last?, filter?, method? })`, `actionLog({ last?, filter?, action?, source? })`, `console`, `fetch`, `Buffer`, `URL`, `setTimeout`
|
|
57
|
+
|
|
58
|
+
The `state` object persists across `exec` calls within the same session — use it to carry values between commands.
|
|
59
|
+
|
|
60
|
+
## CRITICAL: No try/catch in exec
|
|
61
|
+
|
|
62
|
+
**Never use try/catch or .catch() in exec code.** Let errors throw so they surface as exec failures. When an exec fails, you get the full error message (e.g., "intercepts pointer events", "Timeout 30000ms exceeded") — use that to diagnose the problemand write a corrected exec.
|
|
63
|
+
|
|
64
|
+
**Why:** A try/catch inside exec hides failures from you. A click that times out takes 30 seconds — if you retry it in a loop with try/catch, you'll silently burn minutes on the same broken selector with no way to recover. Without try/catch, the error comes back immediately and you can reason about what went wrong.
|
|
65
|
+
|
|
66
|
+
**Instead of try/catch, use check-first patterns:**
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// BAD — silently retries for minutes
|
|
70
|
+
try {
|
|
71
|
+
await btn.click();
|
|
72
|
+
} catch {
|
|
73
|
+
/* retry or ignore */
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// GOOD — check first, fail fast
|
|
77
|
+
if (await btn.isVisible()) await btn.click();
|
|
78
|
+
|
|
79
|
+
// GOOD — check existence before acting
|
|
80
|
+
if ((await page.locator(".cookie-banner").count()) > 0) {
|
|
81
|
+
await page.locator(".cookie-banner button").click();
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If an action fails despite an element being visible, you should not keep retrying it. Instead you can try the following debugging steps:
|
|
86
|
+
|
|
87
|
+
1. Take a snapshot to inspect what's covering the element
|
|
88
|
+
2. Try `{ force: true }` to bypass actionability checks
|
|
89
|
+
3. Try a completely different approach (e.g., opening a dialog via a different button)
|
|
90
|
+
|
|
91
|
+
## Workflow: Browse and Interact
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Open a page
|
|
95
|
+
npx libretto open https://example.com
|
|
96
|
+
|
|
97
|
+
# Interact with elements
|
|
98
|
+
npx libretto exec "await page.locator('button:has-text(\"Sign in\")').click()"
|
|
99
|
+
npx libretto exec "await page.fill('input[name=\"email\"]', 'user@example.com')"
|
|
100
|
+
|
|
101
|
+
# Understand the page — always provide objective and context
|
|
102
|
+
npx libretto snapshot \
|
|
103
|
+
--objective "Find the sign-in form fields and submit button" \
|
|
104
|
+
--context "Navigated to example.com login page. Expecting email/password inputs and a submit button."
|
|
105
|
+
|
|
106
|
+
# Include relevant network calls in context when debugging API interactions
|
|
107
|
+
npx libretto snapshot \
|
|
108
|
+
--objective "Find why the referral list is empty" \
|
|
109
|
+
--context "Logged into eClinicalWorks. Clicked Open Referrals tab. Table appears but shows no rows. Recent POST to /servlet/AjaxServlet returned 200 but with empty body."
|
|
110
|
+
|
|
111
|
+
# Done
|
|
112
|
+
npx libretto close
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Workflow: Save and Restore Login Sessions
|
|
116
|
+
|
|
117
|
+
Profiles persist cookies and localStorage across browser launches. They are saved to `.libretto/profiles/<domain>.json` (git-ignored) and loaded automatically on `open`.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Open a site in headed mode so you can log in manually
|
|
121
|
+
npx libretto open https://portal.example.com --headed
|
|
122
|
+
|
|
123
|
+
# ... manually log in in the browser window ...
|
|
124
|
+
|
|
125
|
+
# Save the session
|
|
126
|
+
npx libretto save portal.example.com
|
|
127
|
+
|
|
128
|
+
# Next time you open this domain, you'll be logged in automatically
|
|
129
|
+
npx libretto open https://portal.example.com
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Workflow: Interactive Debugging
|
|
133
|
+
|
|
134
|
+
When browser automation jobs fail (selectors timing out, clicks not working), use the interactive debugging workflow instead of edit-restart cycles. This reduces iteration time from 5-10 minutes to ~30 seconds.
|
|
135
|
+
|
|
136
|
+
1. Add `page.pause()` before the problematic code section
|
|
137
|
+
2. Start the job with `npx browser-agent start` (debug mode is always enabled locally)
|
|
138
|
+
3. Wait ~60 seconds for the browser to hit the breakpoint
|
|
139
|
+
4. Use `npx libretto exec` (with `--session browser-agent`) to inspect and prototype fixes
|
|
140
|
+
5. Once the fix works, codify it in source files
|
|
141
|
+
6. Restart the job to verify end-to-end
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Start job in background
|
|
145
|
+
npx browser-agent start \
|
|
146
|
+
--job-type pull-open-referrals \
|
|
147
|
+
--tenant-slug hhb \
|
|
148
|
+
--params '{"vendorName":"eClinicalWorks"}'
|
|
149
|
+
|
|
150
|
+
# Inspect page state
|
|
151
|
+
npx libretto exec --session browser-agent "return await page.url();"
|
|
152
|
+
npx libretto snapshot --session browser-agent \
|
|
153
|
+
--objective "Find dropdown menus and their current selections" \
|
|
154
|
+
--context "Browser agent hit breakpoint during pull-open-referrals job. Need to inspect dropdown state."
|
|
155
|
+
|
|
156
|
+
# List dropdown options
|
|
157
|
+
npx libretto exec --session browser-agent "return await page.locator('option').allTextContents();"
|
|
158
|
+
|
|
159
|
+
# Test a fix
|
|
160
|
+
npx libretto exec --session browser-agent "await page.locator('.dropdown-trigger').click(); return 'clicked';"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Snapshot — The Primary Observation Tool
|
|
164
|
+
|
|
165
|
+
The `snapshot` command captures a PNG screenshot + HTML, sends both to a vision model (Gemini Flash), and returns an analysis with Playwright-ready selectors. `--objective` is required for analysis, and `--context` is optional (but recommended for better results). This is the single way to understand what's on the page — use it any time you need to inspect page structure, find elements, or debug what's happening.
|
|
166
|
+
|
|
167
|
+
**Never use `page.screenshot()` via `exec` to understand the page.** Use the `snapshot` command instead — it captures the screenshot, HTML, and sends both to a vision model that returns actionable selectors. Raw screenshots give you an image with no analysis; `snapshot` gives you the answer.
|
|
168
|
+
|
|
169
|
+
### What to Put in `--objective`
|
|
170
|
+
|
|
171
|
+
The objective tells the vision agent what you're looking for. Be specific:
|
|
172
|
+
|
|
173
|
+
- "Find the referral status column in the table"
|
|
174
|
+
- "Find the error message or alert preventing form submission"
|
|
175
|
+
- "Identify all dropdown menus on the page and their current selections"
|
|
176
|
+
|
|
177
|
+
### What to Put in `--context`
|
|
178
|
+
|
|
179
|
+
Context gives the vision agent situational awareness. Include:
|
|
180
|
+
|
|
181
|
+
1. **Where you are** — page, step, state (e.g., "On the eClinicalWorks referral list page")
|
|
182
|
+
2. **What you did** — actions taken (e.g., "Clicked 'Open Referrals' tab, selected department 'Cardiology'")
|
|
183
|
+
3. **What you expect** — desired state (e.g., "Expecting a table of open referrals with patient names")
|
|
184
|
+
4. **Relevant selectors** — any CSS selectors, data-testids, or element identifiers you already know about
|
|
185
|
+
5. **Task context** — what the automation is trying to accomplish overall
|
|
186
|
+
6. **Network calls** — any relevant HTTP requests/responses (e.g., "POST /api/referrals returned 200 with empty array")
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npx libretto snapshot \
|
|
190
|
+
--objective "Find the referral status column in the table" \
|
|
191
|
+
--context "Logged into eClinicalWorks as admin. Navigated to Referrals > Open Referrals tab. Expecting a table of open referrals with columns for patient name, provider, and status."
|
|
192
|
+
|
|
193
|
+
# Debugging example
|
|
194
|
+
npx libretto snapshot \
|
|
195
|
+
--objective "Find the error message or alert" \
|
|
196
|
+
--context "Clicked Submit on the new referral form after filling in all required fields. Expected to see a success confirmation, but the page appears to still be on the form."
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Inspecting Raw DOM with `exec`
|
|
200
|
+
|
|
201
|
+
When the snapshot doesn't give you enough detail — why an element is hidden, what directives or event handlers it has, how it's styled — use `exec` with `page.evaluate` to query the raw DOM directly.
|
|
202
|
+
|
|
203
|
+
- **`outerHTML`** — See the complete markup of an element including all attributes.
|
|
204
|
+
```bash
|
|
205
|
+
npx libretto exec "const el = await page.locator('#myElement').elementHandle(); return await page.evaluate(el => el.outerHTML.substring(0, 500), el);"
|
|
206
|
+
```
|
|
207
|
+
- **Computed styles / parent chain** — Debug why Playwright can't click an element.
|
|
208
|
+
```bash
|
|
209
|
+
npx libretto exec "const el = await page.locator('#myElement').elementHandle(); return await page.evaluate(el => { const chain = []; let n = el; for (let i = 0; i < 8 && n; i++) { const s = getComputedStyle(n); chain.push({ tag: n.tagName, id: n.id, display: s.display, visibility: s.visibility }); n = n.parentElement; } return chain; }, el);"
|
|
210
|
+
```
|
|
211
|
+
- **Any DOM property** — `page.evaluate` gives you full access: `getBoundingClientRect()`, `dataset`, `children`, `classList`, attached event listeners, etc.
|
|
212
|
+
|
|
213
|
+
## Tips
|
|
214
|
+
|
|
215
|
+
- **Never use `page.screenshot()` via `exec`.** Use `npx libretto snapshot` instead — it captures the viewport, sends the screenshot + HTML to a vision model, and returns actionable selectors. The `fullPage` option is especially dangerous — it scrolls the entire page to stitch a screenshot, which can crash JavaScript-heavy pages (especially EMR portals like eClinicalWorks).
|
|
216
|
+
- **Never run `exec` commands in parallel.** Always wait for one `exec` to finish before starting the next. Do not use `run_in_background` for `exec` calls. Running simultaneous `exec` calls opens multiple CDP connections to the same page, which corrupts the page state and kills the browser.
|
|
217
|
+
- `open` and `run` require an available session. If the session is already active, Libretto fails fast and asks you to close the existing session or use a different `--session`.
|
|
218
|
+
- Use `return <value>` in `exec` to print results. Strings print raw; objects print as JSON.
|
|
219
|
+
- For iframe content, access via `page.locator('iframe[name="..."]').contentFrame()`.
|
|
220
|
+
- Multiple sessions allow parallel browser instances: `--session test1`, `--session test2`.
|
|
221
|
+
|
|
222
|
+
## Network Logging
|
|
223
|
+
|
|
224
|
+
Network requests are captured automatically when a browser is opened via `npx libretto open`. All non-static HTTP responses (excluding `.css`, `.js`, `.png`, `.jpg`, `.gif`, `.woff`, `.ico`, `.svg`, and `chrome-extension://` URLs) are logged to `.libretto/sessions/<session>/network.jsonl`.
|
|
225
|
+
|
|
226
|
+
### CLI: `npx libretto network`
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npx libretto network # show last 20 requests
|
|
230
|
+
npx libretto network --last 50 # show last 50
|
|
231
|
+
npx libretto network --filter 'referral|patient' # regex filter on URL
|
|
232
|
+
npx libretto network --method POST # filter by HTTP method
|
|
233
|
+
npx libretto network --clear # truncate the log file
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### In exec: `networkLog()`
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npx libretto exec "return await networkLog()"
|
|
240
|
+
npx libretto exec "return await networkLog({ filter: 'servlet', last: 5 })"
|
|
241
|
+
npx libretto exec "return await networkLog({ method: 'POST' })"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Returns an array of objects with: `ts`, `method`, `url`, `status`, `contentType`, `postData` (POST/PUT/PATCH only, first 2000 chars), `size`, `durationMs`.
|
|
245
|
+
|
|
246
|
+
**Note:** Network logging only works for sessions opened via `npx libretto open`. It does not capture requests for external sessions like `--session browser-agent`.
|
|
247
|
+
|
|
248
|
+
## Action Logging
|
|
249
|
+
|
|
250
|
+
Browser actions are captured automatically when a browser is opened via `npx libretto open`. Both user interactions (manual clicks, typing in the headed browser window) and agent actions (programmatic Playwright API calls via `exec`) are logged to `.libretto/sessions/<session>/actions.jsonl` with a `source` field of `'user'` or `'agent'` to distinguish the two.
|
|
251
|
+
|
|
252
|
+
### CLI: `npx libretto actions`
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npx libretto actions # show last 20 actions
|
|
256
|
+
npx libretto actions --last 50 # show last 50
|
|
257
|
+
npx libretto actions --filter 'button|input' # regex filter on selector/value
|
|
258
|
+
npx libretto actions --action click # filter by action type
|
|
259
|
+
npx libretto actions --source user # only manual user actions
|
|
260
|
+
npx libretto actions --source agent # only programmatic agent actions
|
|
261
|
+
npx libretto actions --clear # truncate the log file
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### In exec: `actionLog()`
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npx libretto exec "return await actionLog()"
|
|
268
|
+
npx libretto exec "return await actionLog({ source: 'user', last: 5 })"
|
|
269
|
+
npx libretto exec "return await actionLog({ action: 'click' })"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Returns an array of objects with: `ts`, `action`, `source` (`'user'` | `'agent'`), `selector`, `value`, `url`, `duration`, `success`, `error`.
|
|
273
|
+
|
|
274
|
+
**Note:** Action logging only works for sessions opened via `npx libretto open`. It does not capture actions for external sessions like `--session browser-agent`.
|
|
275
|
+
|
|
276
|
+
## Workflow: Creating a New Integration
|
|
277
|
+
|
|
278
|
+
Use Libretto CLI interactively to build a brand new integration from scratch. Navigate the real site with the user, discover the network endpoints, and codify the data extraction into a reusable TypeScript script.
|
|
279
|
+
|
|
280
|
+
**IMPORTANT:** Do NOT explore the codebase or research existing code before starting. This skill file and the CLI commands below contain everything you need. Jump straight into using the CLI interactively — ask the user for the URL, open the browser, and start working. The only exception is if the user mentions a specific file or piece of code to reference — then read that specific file first, but nothing more.
|
|
281
|
+
|
|
282
|
+
### Approach Selection
|
|
283
|
+
|
|
284
|
+
By default, use the **preferred ordering of approaches**: try the network-first approach (`page.evaluate(fetch(...))`) first, then fall back to Playwright DOM automation if that doesn't work (see "Integration Approaches" below).
|
|
285
|
+
|
|
286
|
+
**If the user explicitly specifies an approach**, use it instead.
|
|
287
|
+
|
|
288
|
+
As part of starting the session, silently run a **security posture review** using the probes from `integration-approach-selection.md` (in this skill's directory) to assess the site's bot detection, fetch interception, and security posture. This tells you:
|
|
289
|
+
|
|
290
|
+
- Whether `page.evaluate(fetch(...))` is safe (fetch not patched, no aggressive bot detection)
|
|
291
|
+
- Whether `page.on('response', ...)` interception is viable
|
|
292
|
+
- Whether you need to restrict to DOM-only extraction
|
|
293
|
+
|
|
294
|
+
If the security review reveals that the default network-first approach won't work (e.g., fetch is monkey-patched, aggressive bot detection), **adapt your approach accordingly and tell the user what you found and which approach you're switching to.** You don't need to ask permission to switch — just explain what you discovered and proceed.
|
|
295
|
+
|
|
296
|
+
The user may also share context during the session that changes the approach (e.g., they know the site blocks direct fetch). Adapt as needed.
|
|
297
|
+
|
|
298
|
+
### Handling Approach Mismatches
|
|
299
|
+
|
|
300
|
+
The security review tells you what's _safe_, but not necessarily what _works_ for every endpoint or data source on the site. As you build the integration, you may find that the recommended approach doesn't produce usable data for a specific part of the workflow. When this happens, **explain what you found, adapt your approach** for that specific part, and keep going.
|
|
301
|
+
|
|
302
|
+
Common mismatches:
|
|
303
|
+
|
|
304
|
+
- **Unparseable response format** — The fetch call succeeds but returns a proprietary format (RSC wire protocol, protobuf, encrypted payloads) instead of parseable JSON/XML/HTML.
|
|
305
|
+
- **Data not in API responses** — The data is server-rendered into HTML or computed client-side; no network response contains it.
|
|
306
|
+
- **Endpoint requires unpredictable parameters** — CSRF tokens, request signatures, or session values that rotate and aren't easily extractable.
|
|
307
|
+
|
|
308
|
+
These can surface at any point — the first endpoint you try or the fifteenth. Different parts of the same integration often need different approaches.
|
|
309
|
+
|
|
310
|
+
### Starting the Session
|
|
311
|
+
|
|
312
|
+
The browser stays open indefinitely until explicitly closed with `npx libretto close` or by the user closing the window. **Do not** set any timeouts, auto-close timers, or call `close` until the user says the workflow session is done. Ensure that you open the browser in `--headed` mode so the user can see what's happening.
|
|
313
|
+
|
|
314
|
+
If the site requires login, ask the user how auth should work in the generated workflow:
|
|
315
|
+
|
|
316
|
+
1. Save a local profile (recommended for local runs): open in `--headed`, have the user log in manually, run `npx libretto save <domain>`, and generate workflow metadata with `authProfile: { type: "local", domain: "<hostname>" }`.
|
|
317
|
+
2. Use user-managed credential logic in Playwright code (no local profile dependency).
|
|
318
|
+
|
|
319
|
+
If local profile is chosen, include this warning in your generated workflow guidance: local profiles are machine-local (other users/environments will not have them), and sessions can expire so re-login/re-save may be required.
|
|
320
|
+
|
|
321
|
+
### Integration Approaches
|
|
322
|
+
|
|
323
|
+
There are two main approaches for building an integration. **Try the network-first approach first** — it's faster, more reliable, and less brittle. Fall back to Playwright automation if it doesn't work. Be flexible — different parts of the same integration may use different approaches, and a single workflow often mixes them. The user can also explicitly tell you which approach to use.
|
|
324
|
+
|
|
325
|
+
#### Approach 1: Network-First — `page.evaluate(() => fetch(...))` (Try First)
|
|
326
|
+
|
|
327
|
+
Use `page.evaluate(() => fetch(...))` to make requests directly in the browser's JavaScript context — for both extracting data and performing actions (form submissions, API calls, etc.). The requests share the browser's TLS fingerprint, cookies, and origin, so they look identical to requests the site's own JS would make.
|
|
328
|
+
|
|
329
|
+
**Why this is preferred:** Maximum control and reliability. You call exactly the endpoints you want with the parameters you want, skip fragile UI rendering, and get structured data back. No brittle DOM selectors, no multi-step UI sequences that break when the site changes its layout.
|
|
330
|
+
|
|
331
|
+
**How to try it:**
|
|
332
|
+
|
|
333
|
+
1. Use Playwright to navigate the site normally. Network requests are captured automatically.
|
|
334
|
+
2. Check the network log (`npx libretto network` or `networkLog()`) to find API endpoints the site uses.
|
|
335
|
+
3. Recreate a key request with `page.evaluate(() => fetch(...))` and confirm it works.
|
|
336
|
+
|
|
337
|
+
If the fetch call succeeds, this is your approach. You'll still use Playwright for navigation, login, and session setup — but data extraction and actions go through direct fetch calls.
|
|
338
|
+
|
|
339
|
+
**When it won't work:** If `fetch` is monkey-patched, the site detects non-app-originated requests, or the API uses request signatures you can't replicate.
|
|
340
|
+
|
|
341
|
+
#### Approach 2: Playwright Automation (Fallback)
|
|
342
|
+
|
|
343
|
+
If direct fetch calls don't work, fall back to driving the UI with Playwright — clicking elements, filling forms, reading text from the DOM.
|
|
344
|
+
|
|
345
|
+
**How to try it:**
|
|
346
|
+
|
|
347
|
+
1. Navigate to the page.
|
|
348
|
+
2. Use `npx libretto snapshot` to find selectors.
|
|
349
|
+
3. Drive the UI with Playwright locators (`page.locator(...).click()`, `.fill()`, `.textContent()`, etc.).
|
|
350
|
+
|
|
351
|
+
This works regardless of the site's architecture but is slower and more fragile against layout changes.
|
|
352
|
+
|
|
353
|
+
**Supplementing with `page.on('response', ...)`:** When using Playwright automation, you can optionally listen to network responses the browser makes as you navigate — `page.on('response', ...)` lets you capture API data that flows through the site's own code without making extra requests. This is useful when the site has API endpoints but blocks direct fetch calls. Set up listeners before the navigation that triggers the requests. Not all sites will have useful responses to intercept — some are entirely server-rendered.
|
|
354
|
+
|
|
355
|
+
**The workflow for form submissions and data-heavy actions:**
|
|
356
|
+
|
|
357
|
+
1. Use Playwright to fill out the form, select dropdowns, check boxes — whatever the UI requires
|
|
358
|
+
2. **Ask the user for confirmation before submitting** — describe what you're about to submit and wait for approval
|
|
359
|
+
3. Submit the form — network requests are captured automatically (see "Network Logging" above)
|
|
360
|
+
4. Check the captured requests with `npx libretto network --method POST` or `networkLog()`
|
|
361
|
+
5. Inspect the captured request (URL, method, headers, body) to understand the payload structure
|
|
362
|
+
6. Test recreating that request directly via `page.evaluate(() => fetch(...))` — confirm with the user before sending
|
|
363
|
+
7. In the generated production code, skip the form-filling steps and fire the network request directly, parameterized with the relevant input values
|
|
364
|
+
|
|
365
|
+
### Discovering Network Endpoints
|
|
366
|
+
|
|
367
|
+
Network requests are captured automatically in the background (see "Network Logging" above). Use the network log to discover endpoints instead of manually attaching listeners.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# Fill out a form
|
|
371
|
+
npx libretto exec "await page.locator('#department').selectOption('Cardiology'); return 'selected';"
|
|
372
|
+
npx libretto exec "await page.locator('#status').selectOption('Open'); return 'selected';"
|
|
373
|
+
|
|
374
|
+
# ASK THE USER before submitting — describe what will be submitted
|
|
375
|
+
# Then submit and check what requests fired
|
|
376
|
+
npx libretto exec "await page.locator('#submitBtn').click(); await page.waitForTimeout(3000); return 'submitted';"
|
|
377
|
+
npx libretto network --method POST --last 5
|
|
378
|
+
|
|
379
|
+
# Or query the log programmatically
|
|
380
|
+
npx libretto exec "return await networkLog({ method: 'POST', last: 5 })"
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
For page-load requests (data fetched during navigation), just navigate and then check the log:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
npx libretto exec "await page.goto('https://portal.example.com/encounters'); await page.waitForTimeout(3000); return 'loaded';"
|
|
387
|
+
npx libretto network --last 20
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Testing a Captured Endpoint
|
|
391
|
+
|
|
392
|
+
**Before making any `fetch()` call (GET or POST), always confirm with the user first.** These hit real server endpoints with real session auth — a wrong request could submit data, modify records, or trigger side effects. Describe the URL, method, and parameters you want to test and wait for approval.
|
|
393
|
+
|
|
394
|
+
Note: `page.evaluate(() => fetch(...))` works for replaying both fetch-based and XHR-based endpoints — you're making a new request, not replaying the original mechanism.
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
# Recreate the captured request directly — confirm with user first
|
|
398
|
+
npx libretto exec "
|
|
399
|
+
const resp = await page.evaluate(async () => {
|
|
400
|
+
const r = await fetch('/servlet/AjaxServlet', {
|
|
401
|
+
method: 'POST',
|
|
402
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
403
|
+
body: 'action=getReferrals&department=Cardiology&status=Open'
|
|
404
|
+
});
|
|
405
|
+
return await r.text();
|
|
406
|
+
});
|
|
407
|
+
return resp.substring(0, 1000);
|
|
408
|
+
"
|
|
409
|
+
|
|
410
|
+
# Extract session variables (safe — reads window properties, no server call)
|
|
411
|
+
npx libretto exec "
|
|
412
|
+
return await page.evaluate(() => ({
|
|
413
|
+
sessionDID: (window as any).sessionDID,
|
|
414
|
+
userId: (window as any).TrUserId
|
|
415
|
+
}));
|
|
416
|
+
"
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Generating Code
|
|
420
|
+
|
|
421
|
+
After completing interactive exploration, **always generate the TypeScript workflow file before ending the session** — do not wait for the user to ask.
|
|
422
|
+
|
|
423
|
+
**STOP AND ASK BEFORE GENERATING CODE.** Once the interactive workflow is figured out, pause and ask:
|
|
424
|
+
|
|
425
|
+
1. "Are there any existing files or patterns in the codebase you want me to reference?"
|
|
426
|
+
2. "Do you want me to incorporate any of your manual browser interactions from the actions log (`npx libretto actions --source user`) into the generated code?"
|
|
427
|
+
3. "Any other guidance for how the production code should be structured?"
|
|
428
|
+
|
|
429
|
+
Wait for the user's response before proceeding. Then:
|
|
430
|
+
|
|
431
|
+
1. **Read `code-generation-rules.md`** (in this skill's directory) — this is mandatory before writing any code. It contains the authoritative rules for Playwright locator usage, `page.evaluate()` restrictions, network request patterns, and type checking. Do not generate code from memory; always reference this file first.
|
|
432
|
+
2. Run the TypeScript type checker against the file and fix any errors before presenting it as done.
|
|
433
|
+
|
|
434
|
+
## Patient Safety Warning
|
|
435
|
+
|
|
436
|
+
Browser automation jobs process real patient health information. The `npx libretto` CLI executes arbitrary code with full page access. **Never** execute code that submits forms, sends referrals, deletes data, or modifies patient records.
|
|
437
|
+
|
|
438
|
+
See `apps/browser-agent/docs/interactive-debugging-workflow.md` for the complete debugging guide.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Code Generation Rules
|
|
2
|
+
|
|
3
|
+
These rules apply when generating production TypeScript files from interactive browser sessions. Read this file before writing any production code.
|
|
4
|
+
|
|
5
|
+
## Workflow File Structure
|
|
6
|
+
|
|
7
|
+
Generated files must export a `workflow()` instance so they can be run via `npx libretto run <file> <exportName>`. Import `workflow` and its types from `"libretto"`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { workflow, type LibrettoWorkflowContext } from "libretto";
|
|
11
|
+
|
|
12
|
+
type Input = {
|
|
13
|
+
// Define the expected input shape — passed via --params JSON
|
|
14
|
+
query: string;
|
|
15
|
+
maxResults?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Output = {
|
|
19
|
+
// Define what the workflow returns
|
|
20
|
+
results: Array<{ name: string; value: string }>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const myWorkflow = workflow<Input, Output>(
|
|
24
|
+
{
|
|
25
|
+
// If the site requires a saved login session:
|
|
26
|
+
authProfile: { type: "local", domain: "example.com" },
|
|
27
|
+
// Omit authProfile if no login is needed
|
|
28
|
+
},
|
|
29
|
+
async (ctx: LibrettoWorkflowContext, input: Input): Promise<Output> => {
|
|
30
|
+
const { page } = ctx;
|
|
31
|
+
|
|
32
|
+
// workflow logic here — use ctx.page, ctx.context, ctx.browser
|
|
33
|
+
await page.goto("https://example.com");
|
|
34
|
+
// ...
|
|
35
|
+
|
|
36
|
+
return { results: [] };
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key points:**
|
|
42
|
+
|
|
43
|
+
- The named export (e.g., `myWorkflow`) is what you pass as the second arg to `npx libretto run ./file.ts myWorkflow`
|
|
44
|
+
- `ctx` provides `page`, `context`, `browser`, `session`, `logger`, `headless`, `integrationPath`, `exportName`
|
|
45
|
+
- `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI
|
|
46
|
+
- If `authProfile` is set with a domain, libretto loads the saved browser profile for that domain (created via `npx libretto save <domain>`)
|
|
47
|
+
- The browser is launched and closed automatically by the CLI — do not launch or close it in the handler
|
|
48
|
+
|
|
49
|
+
## Playwright Locators for DOM Interaction
|
|
50
|
+
|
|
51
|
+
Generated code must use Playwright locator APIs for all DOM interactions. Do not use `page.evaluate()` with `document.querySelector`, `querySelectorAll`, `textContent`, `click()`, or other DOM APIs when a Playwright locator can do the same thing.
|
|
52
|
+
|
|
53
|
+
During the interactive `exec` phase, `page.evaluate` is fine for quick prototyping. In generated production code, translate those patterns into Playwright locators.
|
|
54
|
+
|
|
55
|
+
### Translation Table
|
|
56
|
+
|
|
57
|
+
| Operation | Interactive (`exec`) | Production file |
|
|
58
|
+
| ---------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
59
|
+
| Click | `page.evaluate(() => document.getElementById('x').click())` | `page.locator('#x').click()` |
|
|
60
|
+
| Check state | `page.evaluate(() => el.checked)` | `page.locator('#x').isChecked()` |
|
|
61
|
+
| Read text | `page.evaluate(() => el.textContent)` | `page.locator('#x').textContent()` |
|
|
62
|
+
| Read all text | `querySelectorAll(...).map(e => e.textContent)` | `page.locator('.items').allTextContents()` |
|
|
63
|
+
| Element position | `el.getBoundingClientRect()` | `page.locator('#x').boundingBox()` |
|
|
64
|
+
| Inline styles | `el.style.top` | `page.locator('#x').getAttribute('style')` |
|
|
65
|
+
| Count elements | `querySelectorAll(...).length` | `page.locator('.items').count()` |
|
|
66
|
+
| Select dropdown | `selectEl.value = '...'` | `page.locator('select').selectOption('...')` |
|
|
67
|
+
| Iterate elements | `querySelectorAll(...).forEach(...)` | `const items = await locator.all(); for (const item of items) { ... }` |
|
|
68
|
+
| Scoped query | `parent.querySelector('.child')` | `parentLocator.locator('.child').textContent()` |
|
|
69
|
+
| Batch extraction | `querySelectorAll('.item').forEach(e => { ... })` | `for (const item of await locator.all()) { const text = await item.locator('.text').textContent(); ... }` |
|
|
70
|
+
|
|
71
|
+
### Anti-Patterns
|
|
72
|
+
|
|
73
|
+
These patterns come up frequently during interactive sessions and should not carry over into production code:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// DON'T — batch-read via evaluate string
|
|
77
|
+
const data = await page.evaluate(`(() => {
|
|
78
|
+
const posts = document.querySelectorAll('.post');
|
|
79
|
+
return Array.from(posts).map(p => ({
|
|
80
|
+
name: p.querySelector('.name')?.textContent,
|
|
81
|
+
content: p.querySelector('.content')?.textContent,
|
|
82
|
+
}));
|
|
83
|
+
})()`);
|
|
84
|
+
|
|
85
|
+
// DO — Playwright locators with a loop
|
|
86
|
+
const posts = await page.locator('.post').all();
|
|
87
|
+
for (const post of posts) {
|
|
88
|
+
const name = await post.locator('.name').textContent();
|
|
89
|
+
const content = await post.locator('.content').textContent();
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// DON'T — evaluate to count elements
|
|
95
|
+
const count = await el.evaluate(`(el) => el.querySelectorAll('.item').length`);
|
|
96
|
+
|
|
97
|
+
// DO
|
|
98
|
+
const count = await el.locator('.item').count();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// DON'T — evaluate to read scoped text
|
|
103
|
+
const text = await post.evaluate(
|
|
104
|
+
`(el) => el.querySelector('[data-view-name="foo"]')?.textContent`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// DO
|
|
108
|
+
const text = await post.locator('[data-view-name="foo"]').textContent();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### When `page.evaluate()` Is Acceptable
|
|
112
|
+
|
|
113
|
+
Use `page.evaluate()` only for operations that have no Playwright locator equivalent:
|
|
114
|
+
|
|
115
|
+
1. **Browser-native APIs** — `getComputedStyle()`, `window.*` globals, `document.cookie`, scroll position
|
|
116
|
+
2. **In-browser `fetch()` calls** — making HTTP requests from the browser context
|
|
117
|
+
3. **Parsing operations** — using `DOMParser` to parse HTML/XML strings inside the browser
|
|
118
|
+
|
|
119
|
+
A quick test: if the evaluate body contains `querySelector`, `querySelectorAll`, `textContent`, `click()`, `getAttribute()`, or iterates DOM elements, it should be rewritten with Playwright locators.
|
|
120
|
+
|
|
121
|
+
When `page.evaluate()` is used for the acceptable cases above, use a string expression to avoid DOM type errors:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const data = (await page.evaluate(`(() => {
|
|
125
|
+
const style = getComputedStyle(document.documentElement);
|
|
126
|
+
return style.getPropertyValue('--brand-color');
|
|
127
|
+
})()`)) as string;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Do not use `/// <reference lib="dom" />` or add `"dom"` to the tsconfig lib — this project's tsconfig intentionally excludes DOM types.
|
|
131
|
+
|
|
132
|
+
## Network Request Methods
|
|
133
|
+
|
|
134
|
+
When codifying network-based data extraction or form submissions, wrap `page.evaluate(() => fetch(...))` calls in typed methods on a shared API client class:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
class ApiClient {
|
|
138
|
+
constructor(private page: Page) {}
|
|
139
|
+
|
|
140
|
+
private async apiFetch(
|
|
141
|
+
url: string,
|
|
142
|
+
options?: { method?: string; body?: string },
|
|
143
|
+
): Promise<string> {
|
|
144
|
+
return await this.page.evaluate(
|
|
145
|
+
async ({ url, method, body }) => {
|
|
146
|
+
const init: RequestInit = { method: method ?? "GET" };
|
|
147
|
+
if (body) {
|
|
148
|
+
init.headers = {
|
|
149
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
150
|
+
};
|
|
151
|
+
init.body = body;
|
|
152
|
+
}
|
|
153
|
+
const response = await fetch(url, init);
|
|
154
|
+
if (!response.ok) throw new Error(`${response.status} for ${url}`);
|
|
155
|
+
return await response.text();
|
|
156
|
+
},
|
|
157
|
+
{ url, method: options?.method, body: options?.body },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async fetchReferralList(status: string): Promise<Referral[]> {
|
|
162
|
+
const raw = await this.apiFetch(`/api/referrals?status=${status}`);
|
|
163
|
+
// parse and return typed data
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
One method per endpoint. No try-catch in API methods — let errors propagate to the orchestrator. Parse XML/HTML inside `page.evaluate()` with `DOMParser`. Use string expressions for `page.evaluate()` to avoid DOM type errors.
|
|
169
|
+
|
|
170
|
+
## Comments
|
|
171
|
+
|
|
172
|
+
Add comments throughout generated code to explain what each logical block is doing. Comments should describe **intent**, not restate the code. Group related actions under a single comment rather than commenting every line.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Log in with credentials
|
|
176
|
+
await page.locator('#username').fill(user);
|
|
177
|
+
await page.locator('#password').fill(pass);
|
|
178
|
+
await page.locator('#login').click();
|
|
179
|
+
|
|
180
|
+
// Extract author and content from each feed post
|
|
181
|
+
const posts = await page.locator('.post').all();
|
|
182
|
+
for (const post of posts) {
|
|
183
|
+
const name = await post.locator('.name').textContent();
|
|
184
|
+
const content = await post.locator('.content').textContent();
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Type Checking
|
|
189
|
+
|
|
190
|
+
The generated file must pass `npx tsc --noEmit` before it's considered done. If there are DOM type errors (`document`, `HTMLElement`, `getComputedStyle`), convert to locator APIs or string-expression `page.evaluate()`.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Integration Approach Selection Guide
|
|
2
|
+
|
|
3
|
+
**Purpose:** You are connected to a live Chrome session on a target website. Your job is to probe the site for bot detection measures, assess its security posture, and determine the best integration strategy for data extraction. All strategies use Playwright for browser control — the question is what to **prioritize** for data capture: in-browser fetch calls, passive network interception, or DOM extraction.
|
|
4
|
+
|
|
5
|
+
After completing the probes below, produce a **Site Assessment Summary** (see the output format at the end of this document).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Probing the Site
|
|
10
|
+
|
|
11
|
+
Run these probes to build a picture of the site's detection posture. The examples below are starting points — use your judgment to investigate further based on what you find. Sites may use detection methods not listed here.
|
|
12
|
+
|
|
13
|
+
### Probe 1: Bot Protection Services & Security Signals
|
|
14
|
+
|
|
15
|
+
Look for signs that the site uses bot protection — either a third-party service or custom detection. There is no complete list of indicators; these are common examples.
|
|
16
|
+
|
|
17
|
+
**Cookies to look for (examples, not exhaustive):**
|
|
18
|
+
|
|
19
|
+
| Cookie Pattern | Associated Service |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `_abck` | Akamai Bot Manager |
|
|
22
|
+
| `_px*` | PerimeterX (HUMAN) |
|
|
23
|
+
| `datadome` | DataDome |
|
|
24
|
+
| `cf_clearance` | Cloudflare |
|
|
25
|
+
| `_imp_apg_r_*` | Shape Security (F5) |
|
|
26
|
+
| `x-kpsdk-*` | Kasada |
|
|
27
|
+
|
|
28
|
+
But don't just check this list. Examine **all** cookies on the page — look for any cookies with obfuscated names, telemetry-related prefixes, or values that look like fingerprint hashes or encrypted tokens. Unknown security cookies are still security cookies.
|
|
29
|
+
|
|
30
|
+
**Global variables to check (examples):**
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// Known telemetry globals — but probe broadly, not just these
|
|
34
|
+
window._pxAppId // PerimeterX
|
|
35
|
+
window.bmak // Akamai
|
|
36
|
+
window.ddjskey // DataDome
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Also examine the page's scripts: look at the first `<script>` tags in the document source, check what external domains scripts load from (e.g., `*.akamaized.net`, `*.perimeterx.net`, `*.datadome.co`, `*.kasada.io`). Bot protection scripts are typically injected before any application code.
|
|
40
|
+
|
|
41
|
+
**Challenge pages:**
|
|
42
|
+
|
|
43
|
+
Check if the page is showing a challenge or interstitial instead of real content — "Checking your browser...", CAPTCHA iframes, blank pages with only a spinner. These indicate active bot protection that has already been triggered.
|
|
44
|
+
|
|
45
|
+
**General guidance:** The goal is to determine whether the site has bot protection and roughly how aggressive it is. Don't limit yourself to known signatures — look at the overall page behavior, unusual scripts, and anything that seems like security telemetry.
|
|
46
|
+
|
|
47
|
+
### Probe 2: Fetch / XHR Interception
|
|
48
|
+
|
|
49
|
+
Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. If it has, making your own fetch calls from `page.evaluate()` is risky because the site can inspect call stacks and detect calls that don't originate from its own code.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
// Check if fetch has been wrapped
|
|
53
|
+
window.fetch.toString()
|
|
54
|
+
// Native: "function fetch() { [native code] }"
|
|
55
|
+
// Patched: shows actual JavaScript source
|
|
56
|
+
|
|
57
|
+
// Check XMLHttpRequest
|
|
58
|
+
XMLHttpRequest.prototype.open.toString()
|
|
59
|
+
|
|
60
|
+
// Check property descriptors for tampering
|
|
61
|
+
Object.getOwnPropertyDescriptor(window, 'fetch')
|
|
62
|
+
// Normal: { value: ƒ, writable: true, enumerable: true, configurable: true }
|
|
63
|
+
|
|
64
|
+
// Proxy-based wrapping is harder to detect — native fetch has no prototype
|
|
65
|
+
window.fetch.hasOwnProperty('prototype') // true may indicate a Proxy wrapper
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Important:** Some sites use `Proxy` to wrap fetch, which makes `toString()` still return `"[native code]"`. The prototype check is a heuristic, not definitive. If you see any sign of fetch interception, treat it as patched.
|
|
69
|
+
|
|
70
|
+
### Probe 3: Behavioral Monitoring
|
|
71
|
+
|
|
72
|
+
Look for signs that the site collects behavioral telemetry (mouse movements, keystrokes, scroll patterns). Heavy monitoring means you should use natural, human-like interaction patterns when driving the UI.
|
|
73
|
+
|
|
74
|
+
Things to check:
|
|
75
|
+
- Unusually large numbers of event listeners on `document` or `body` for `mousemove`, `keydown`, `scroll`, `touchstart`, `click`
|
|
76
|
+
- Known telemetry collection scripts
|
|
77
|
+
- `MutationObserver` instances watching the DOM for injected elements
|
|
78
|
+
- `requestAnimationFrame` loops that aren't tied to visible animations
|
|
79
|
+
|
|
80
|
+
If you're in a DevTools context, `getEventListeners(document)` is the quickest way to assess this. Otherwise, use heuristics — heavy behavioral monitoring usually correlates with enterprise bot protection from Probe 1.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Choosing a Data Capture Strategy
|
|
85
|
+
|
|
86
|
+
Every integration uses Playwright to control the browser. The question is what to **prioritize** for getting data out. In practice, most integrations use a mix — you'll always need some Playwright interaction for navigation, login flows, cookie consent, etc. The strategies below describe what to lean on for the core data extraction.
|
|
87
|
+
|
|
88
|
+
### Strategy A: Prioritize `page.evaluate(fetch(...))`
|
|
89
|
+
|
|
90
|
+
Make fetch calls directly from within the browser's JavaScript context. The requests share the browser's TLS fingerprint, cookies, and origin — they look identical to requests the site's own JS would make.
|
|
91
|
+
|
|
92
|
+
**When to prioritize this:**
|
|
93
|
+
- No enterprise bot protection detected
|
|
94
|
+
- `fetch` is NOT monkey-patched
|
|
95
|
+
- You've identified API endpoints that return the data you need
|
|
96
|
+
- You need data that requires many API calls (deep pagination, bulk queries) where driving the UI would be slow
|
|
97
|
+
|
|
98
|
+
**Why:** Maximum control and efficiency. You call exactly the endpoints you want with the parameters you want, skip UI rendering, and get structured JSON back. On sites without aggressive detection, this is the fastest and cleanest approach.
|
|
99
|
+
|
|
100
|
+
**Risk:** If the site monitors fetch call stacks (Layer 4 detection), your calls will be flagged because they don't originate from the site's bundled code. This is uncommon but exists on high-security sites.
|
|
101
|
+
|
|
102
|
+
**You'll still use Playwright for:** Initial navigation, login/auth flows, cookie consent, and any UI interactions needed to establish session state before making fetch calls.
|
|
103
|
+
|
|
104
|
+
### Strategy B: Prioritize `page.on('response', ...)` (Passive Interception)
|
|
105
|
+
|
|
106
|
+
Listen to network responses that the browser naturally makes as you navigate. You don't make any extra requests — you capture data flowing through the site's own API calls.
|
|
107
|
+
|
|
108
|
+
**When to prioritize this:**
|
|
109
|
+
- Enterprise bot protection is detected
|
|
110
|
+
- `fetch` IS monkey-patched
|
|
111
|
+
- The site's normal UI flow triggers API calls that return the data you need
|
|
112
|
+
- You want to minimize detection risk as much as possible
|
|
113
|
+
|
|
114
|
+
**Why:** Zero additional network risk. The requests that happen are the ones the site's own code triggers. You're just listening. No anomalous call stacks, no unexpected request patterns, no extra fetch calls for monitoring to flag.
|
|
115
|
+
|
|
116
|
+
**Trade-off:** You only get data the page naturally loads. If you need page 50 of results, you have to click "next" 49 times via Playwright. You must set up listeners before the navigation that triggers the requests.
|
|
117
|
+
|
|
118
|
+
**You'll still use Playwright for:** All navigation and interaction to trigger the data-bearing API calls, plus any data that isn't available via intercepted responses (DOM-only content).
|
|
119
|
+
|
|
120
|
+
### Strategy C: Prioritize Playwright DOM Extraction
|
|
121
|
+
|
|
122
|
+
Extract data directly from the rendered page using selectors and `page.evaluate()` to read DOM content.
|
|
123
|
+
|
|
124
|
+
**When to prioritize this:**
|
|
125
|
+
- Data is server-rendered (no JSON API calls observed)
|
|
126
|
+
- The site doesn't expose the data you need via any API
|
|
127
|
+
- You need visual/layout information that only exists in the DOM
|
|
128
|
+
- As a fallback when Strategies A and B can't get specific pieces of data
|
|
129
|
+
|
|
130
|
+
**Why:** Works regardless of the site's API architecture. If the data is visible on the page, you can extract it.
|
|
131
|
+
|
|
132
|
+
**Trade-off:** Slower, fragile against DOM changes, and you only get data the UI renders (which may be less than what API responses contain).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Decision Summary
|
|
137
|
+
|
|
138
|
+
| Site Profile | Primary Strategy | Supplement With |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| No bot protection, fetch not patched | **A** (`page.evaluate(fetch)`) | Playwright for navigation/auth |
|
|
141
|
+
| No bot protection, fetch IS patched | **B** (`page.onResponse`) | Playwright for navigation; DOM extraction as fallback |
|
|
142
|
+
| Bot protection detected, fetch not patched | **B** (`page.onResponse`) | Playwright for all navigation; cautious use of `page.evaluate(fetch)` only if needed |
|
|
143
|
+
| Bot protection detected, fetch IS patched | **B** (`page.onResponse`) | Playwright for all navigation; DOM extraction as fallback |
|
|
144
|
+
| Server-rendered content (no API calls) | **C** (DOM extraction) | Playwright for all interaction |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Output: Site Assessment Summary
|
|
149
|
+
|
|
150
|
+
After running the probes, produce a summary in this format. **Do NOT include a final strategy recommendation.** The security assessment determines what's *safe to use*, not what will *work*. Present this to the user for input, then use the safe approaches as you build the integration — adapting if specific endpoints don't work as expected (see "Handling Approach Mismatches" in SKILL.md).
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
## Site Assessment: [site URL]
|
|
154
|
+
|
|
155
|
+
### Bot Detection Profile
|
|
156
|
+
- **Enterprise bot protection:** [None detected / Detected — describe what you found (service name if identifiable, cookies, scripts, telemetry globals)]
|
|
157
|
+
- **Fetch/XHR interception:** [Native (not patched) / Patched — describe what you found]
|
|
158
|
+
- **Behavioral monitoring:** [None detected / Light / Heavy — describe indicators]
|
|
159
|
+
- **Challenge pages:** [None / Present — describe type (CAPTCHA, interstitial, etc.)]
|
|
160
|
+
- **Overall security posture:** [None / Low / Moderate / High / Very High]
|
|
161
|
+
|
|
162
|
+
### API Surface
|
|
163
|
+
- **API calls observed:** [List key endpoints discovered, or "None — content appears server-rendered"]
|
|
164
|
+
- **Data format:** [JSON / GraphQL / HTML fragments / Other — note if any responses use proprietary/binary formats]
|
|
165
|
+
- **Pagination:** [Describe how pagination works if applicable]
|
|
166
|
+
|
|
167
|
+
### Safe Approaches
|
|
168
|
+
- **`page.evaluate(fetch(...))`:** [Safe / Unsafe — brief rationale based on fetch patching, bot detection, etc.]
|
|
169
|
+
- **`page.on('response', ...)`:** [Viable / Not viable — note if response formats are parseable or proprietary]
|
|
170
|
+
- **DOM extraction:** [Always available as fallback]
|
|
171
|
+
- **Interaction notes:** [any behavioral precautions — natural mouse movements, typing delays, etc.]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Important:** This assessment tells you which tools are in your toolbox. Present it to the user, get their input, then start building the integration using the safe approaches.
|