poltertab 1.3.0 → 1.5.0-beta.1
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/README.md +77 -23
- package/mcp-server/bridge.js +593 -0
- package/mcp-server/config.js +85 -0
- package/mcp-server/extract-all.js +159 -0
- package/mcp-server/index.js +68 -931
- package/mcp-server/memory.js +81 -0
- package/mcp-server/output.js +103 -0
- package/mcp-server/tools.js +439 -0
- package/package.json +1 -2
- package/skills/browser-navigation-strategy/SKILL.md +162 -34
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: browser-navigation-strategy
|
|
3
|
-
description: Use when driving a real browser with PolterTab's browser_* tools — navigating, scraping, or clicking through a site whose DOM is virtualized, obfuscated, shadow-rooted, or iframe-sandboxed, and selectors keep going stale.
|
|
3
|
+
description: Use when driving a real browser with PolterTab's browser_* tools — extracting repeating records, paginating a listing, navigating, scraping, or clicking through a site whose DOM is virtualized, obfuscated, shadow-rooted, or iframe-sandboxed, and selectors keep going stale.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Browser Navigation Strategy
|
|
@@ -9,49 +9,140 @@ PolterTab drives the user's real Chrome profile. Sessions are already logged in,
|
|
|
9
9
|
and every action happens in a window the user can see. That makes it powerful and
|
|
10
10
|
unforgiving: a wrong click lands on a real account.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Pick the right tool before you start
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
| What you want | Use |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| Many similar records on a page (cards, rows, listings) | `browser_extract` |
|
|
17
|
+
| Those records across many pages | `browser_extract_all` |
|
|
18
|
+
| One element's text | `browser_get_text` |
|
|
19
|
+
| Page title, og: tags, schema.org data | `browser_scrape` with `fields` |
|
|
20
|
+
| To find out what a page contains | `browser_snapshot` |
|
|
21
|
+
| Data the DOM does not hold (virtualized/obfuscated) | intercept + `browser_get_network_state` |
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
Getting this wrong is the most expensive mistake available. Scraping 100 records
|
|
24
|
+
one field at a time costs a round-trip per page and leaves you reassembling
|
|
25
|
+
records by hand; `browser_extract_all` is one call.
|
|
21
26
|
|
|
22
|
-
##
|
|
27
|
+
## Repeating records: use browser_extract
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
A flat scrape returns a list per selector, so you have to work out which value
|
|
30
|
+
belongs to which record. That works until a field is optional — then one missing
|
|
31
|
+
value shifts every later record and the result looks completely plausible.
|
|
32
|
+
|
|
33
|
+
`browser_extract` takes a container and resolves each field *inside* it:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"record": ".agent-card",
|
|
38
|
+
"fields": {
|
|
39
|
+
"name": { "sel": "a.agent-card-name", "get": "text" },
|
|
40
|
+
"url": { "sel": "a.agent-card-name", "get": "href" },
|
|
41
|
+
"phone": { "sel": "a[href^='tel:']", "get": "href", "strip": "tel:" },
|
|
42
|
+
"socials": { "sel": "a.social-button", "get": "href", "many": true }
|
|
43
|
+
},
|
|
44
|
+
"anchor": "url",
|
|
45
|
+
"output_file": "agents.csv"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- A missing field is `null`. It never shifts.
|
|
50
|
+
- `sel` is relative to the record; omit it (or `"."`) for the record itself.
|
|
51
|
+
- `get`: `text` (default), `href`, `src`, or any attribute name. `href`/`src`
|
|
52
|
+
come back absolute.
|
|
53
|
+
- `anchor` names the field every real record has. Records without it — the
|
|
54
|
+
placeholder cards padding a last page — are dropped and counted.
|
|
55
|
+
|
|
56
|
+
**Read the response before trusting it.** `fill_rates` and `warnings` tell you
|
|
57
|
+
whether the spec is right:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
fill_rates: { name: 12, phone: 10, socials: 0 }
|
|
61
|
+
warnings: ["socials: 0/12 within record scope, but 40 matches page-wide
|
|
62
|
+
for a.social-button — record boundary likely too narrow"]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That warning means the container you picked excludes a sibling holding the
|
|
66
|
+
field. Widen `record` and re-run. Without it, an empty column reads as "these
|
|
67
|
+
records have no socials" — confidently wrong.
|
|
68
|
+
|
|
69
|
+
A field at 0 with *no* page-wide matches means the selector is wrong, not the
|
|
70
|
+
boundary. The message distinguishes the two; act on which one you got.
|
|
71
|
+
|
|
72
|
+
## Paginating: use browser_extract_all
|
|
73
|
+
|
|
74
|
+
Same spec plus a `url_template` containing `{page}`. It runs the loop itself, so
|
|
75
|
+
pages cost a browser round-trip instead of a model round-trip.
|
|
76
|
+
|
|
77
|
+
Dedup on `key` — a stable per-record id, the detail URL or an element `id`, never
|
|
78
|
+
position. It halts on the first of `limit_reached`, `empty_page`,
|
|
79
|
+
`duplicate_page`, `fill_rate_deviation`, or `max_pages`, and always says which.
|
|
80
|
+
|
|
81
|
+
Two of those are worth understanding:
|
|
82
|
+
|
|
83
|
+
- **`duplicate_page`** — the page returned records already seen. This is what a
|
|
84
|
+
site does with a param it does not recognise: it serves page 1 again. Without
|
|
85
|
+
the check you would collect the same 12 records eight times.
|
|
86
|
+
- **`fill_rate_deviation`** — a field well-populated on page 1 collapsed on a
|
|
87
|
+
later page, so the learned spec no longer fits. Everything collected so far is
|
|
88
|
+
returned; re-run from `start_page` with a spec covering both layouts. Set
|
|
89
|
+
`fill_tolerance: 0` if a site is legitimately heterogeneous.
|
|
90
|
+
|
|
91
|
+
## Take the free structured data first
|
|
27
92
|
|
|
28
|
-
|
|
93
|
+
Before writing any selector, try:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{ "fields": ["meta", "jsonld"] }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
on `browser_scrape`. `title`, every `og:`/`twitter:` tag, and any schema.org
|
|
100
|
+
`ld+json` blob come back with no selectors at all. Real estate, jobs, products,
|
|
101
|
+
events and profile pages very often ship a complete typed record this way —
|
|
102
|
+
name, telephone, email, postal address, employer — which beats any selector you
|
|
103
|
+
could write and does not break on redesign.
|
|
104
|
+
|
|
105
|
+
This is the cheapest call available on a detail page. Try it before building an
|
|
106
|
+
enrichment scrape.
|
|
107
|
+
|
|
108
|
+
## Prefer the network over the DOM — when the DOM is the problem
|
|
109
|
+
|
|
110
|
+
On a virtualized list (LinkedIn, most infinite feeds), the DOM holds only the
|
|
111
|
+
rows on screen, under class names that change between deploys. There, the
|
|
112
|
+
network is the reliable path:
|
|
29
113
|
|
|
30
114
|
1. `browser_set_intercept_patterns` if the site uses endpoints outside the
|
|
31
115
|
defaults (`graphql`, `/api/`, `voyager`, `feed`).
|
|
32
116
|
2. `browser_smart_scroll` to trigger lazy loading. It yields until new requests
|
|
33
117
|
land, so you do not have to guess a wait.
|
|
34
|
-
3. `browser_get_network_state` to read the
|
|
35
|
-
|
|
118
|
+
3. `browser_get_network_state` to read the JSON the page itself received.
|
|
119
|
+
|
|
120
|
+
**But check first that there is a network path at all.** Plenty of sites are
|
|
121
|
+
server-rendered: the records ship in the initial HTML and no XHR ever fires.
|
|
122
|
+
Chasing the network there costs several calls and returns an empty buffer that
|
|
123
|
+
looks identical to "you set the wrong pattern". If `browser_extract` already
|
|
124
|
+
returns full records, or `fields: ["jsonld"]` has the data, the page is
|
|
125
|
+
server-rendered — use the DOM and move on.
|
|
36
126
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
127
|
+
The default intercept patterns include `/api/`, which matches analytics and maps
|
|
128
|
+
traffic on most sites. An almost-empty-looking buffer full of Google requests is
|
|
129
|
+
that, not your data.
|
|
40
130
|
|
|
41
131
|
## Shadow DOM, iframes, and late modals
|
|
42
132
|
|
|
43
133
|
PolterTab handles these without special flags, which is worth knowing so you do
|
|
44
134
|
not work around problems it already solves:
|
|
45
135
|
|
|
46
|
-
- **Shadow roots** — including closed ones — are pierced automatically
|
|
47
|
-
|
|
136
|
+
- **Shadow roots** — including closed ones — are pierced automatically, in
|
|
137
|
+
`browser_extract`'s record scope too.
|
|
48
138
|
- **Child frames** are searched in parallel when the top frame misses, so an
|
|
49
139
|
element inside a sandboxed iframe is reachable by plain selector.
|
|
50
140
|
- **Late-rendering elements** get retried for ~3s. A modal that mounts a beat
|
|
51
141
|
after the click that opened it does not need a manual wait.
|
|
52
142
|
|
|
53
|
-
|
|
54
|
-
|
|
143
|
+
`Element not found` after that window means "not on this page", not "try again".
|
|
144
|
+
`browser_extract` does not throw for this — it returns `records_found: 0` with
|
|
145
|
+
the warnings above, which tell you more than an error would.
|
|
55
146
|
|
|
56
147
|
## Selectors, in order of durability
|
|
57
148
|
|
|
@@ -63,32 +154,69 @@ retry window. Treat that as "not on this page", not "try again immediately".
|
|
|
63
154
|
Long generated class chains (`.css-1x2y3z4`) are the worst of both worlds:
|
|
64
155
|
brittle and unreadable. Reach for a `@e` ref from the last snapshot instead.
|
|
65
156
|
|
|
157
|
+
## Keep payloads out of the context window
|
|
158
|
+
|
|
159
|
+
Every read tool takes `output_file`. The payload goes to
|
|
160
|
+
`~/.poltertab/downloads/` and you get back row count, field names, fill rates
|
|
161
|
+
and two sample records. Use it for anything bigger than a glance.
|
|
162
|
+
|
|
163
|
+
`.csv` and `.jsonl` are written in those formats, so the file is the deliverable
|
|
164
|
+
— never re-type extracted records into a file by hand, which costs more than the
|
|
165
|
+
extraction did.
|
|
166
|
+
|
|
167
|
+
`browser_snapshot` on a real page runs to tens of KB. Narrow it with
|
|
168
|
+
`interactive_only` or `max_nodes`, or send it to `output_file`.
|
|
169
|
+
|
|
170
|
+
Text fields are cut at `max_text` (default 500) and the result says when it
|
|
171
|
+
happened. Raise it rather than assuming you have the whole value.
|
|
172
|
+
|
|
173
|
+
## Known limits — do not fight these
|
|
174
|
+
|
|
175
|
+
- **Records spanning sibling elements.** `browser_extract` needs one container
|
|
176
|
+
per record. A table where an item's fields live in two sibling `<tr>`s (Hacker
|
|
177
|
+
News is the canonical case) has no such container. Extract twice and join on a
|
|
178
|
+
shared id — never on position.
|
|
179
|
+
- **Pagination that is not a URL parameter.** `browser_extract_all` only drives
|
|
180
|
+
`{page}` in a URL. Infinite scroll, a "Load more" button, and cursor tokens
|
|
181
|
+
need `browser_smart_scroll` or clicking, in a loop you run yourself.
|
|
182
|
+
- **Virtualized lists.** `browser_extract` reads the DOM, so it sees only the
|
|
183
|
+
rendered window. Use the network path.
|
|
184
|
+
- **`output_file` writes only to `~/.poltertab/downloads/`.** A path is reduced
|
|
185
|
+
to its basename; the returned `file` is where it actually went.
|
|
186
|
+
|
|
66
187
|
## Carry knowledge between sessions
|
|
67
188
|
|
|
68
189
|
`browser_get_site_memory(hostname)` at the start of work on a site, and
|
|
69
|
-
`browser_save_site_memory` the moment you solve something non-obvious
|
|
70
|
-
|
|
71
|
-
|
|
190
|
+
`browser_save_site_memory` the moment you solve something non-obvious. Save the
|
|
191
|
+
record root, the field spec that worked, the pagination parameter, and any
|
|
192
|
+
obstacle with its fix. `www.` is normalized and a full URL is accepted, so
|
|
193
|
+
lookups do not miss.
|
|
72
194
|
|
|
73
|
-
|
|
74
|
-
the
|
|
195
|
+
A note saying "this page is tricky" helps nobody. Save what you would need to
|
|
196
|
+
skip the discovery entirely next time.
|
|
75
197
|
|
|
76
198
|
## Multiple tabs
|
|
77
199
|
|
|
78
200
|
`browser_session_create` gives a named tab you can return to. Every page-facing
|
|
79
|
-
tool takes `session` (or `tabId`)
|
|
80
|
-
last tab it navigated, falling back to the active tab — fine for single-track
|
|
81
|
-
work, ambiguous the moment two flows interleave. Name your sessions when running
|
|
82
|
-
more than one.
|
|
201
|
+
tool takes `session` (or `tabId`).
|
|
83
202
|
|
|
84
|
-
|
|
203
|
+
Omit both and the first `browser_navigate` opens its own tab — it does not
|
|
204
|
+
commandeer the tab the user is looking at — and later calls reuse it. That is
|
|
205
|
+
fine for single-track work and ambiguous the moment two flows interleave, so
|
|
206
|
+
name your sessions when running more than one. Close what you opened with
|
|
207
|
+
`browser_session_close`.
|
|
85
208
|
|
|
86
209
|
## Pitfalls
|
|
87
210
|
|
|
211
|
+
- Do not scrape repeating records field-by-field. Use `browser_extract`, and
|
|
212
|
+
read its `warnings`.
|
|
213
|
+
- Do not zip flat arrays into records by position. One optional field silently
|
|
214
|
+
ruins the whole dataset.
|
|
215
|
+
- Do not loop pages yourself when `url_template` covers the site.
|
|
88
216
|
- Do not use `browser_get_text` or `browser_scrape` on virtualized lists. Use
|
|
89
217
|
`browser_smart_scroll` then `browser_get_network_state`.
|
|
90
|
-
- Do not invent intercept patterns
|
|
91
|
-
site
|
|
218
|
+
- Do not invent intercept patterns, and do not assume an empty buffer means you
|
|
219
|
+
asked wrong — the site may be server-rendered.
|
|
92
220
|
- Do not paste large scraped payloads into the conversation. Use `output_file`.
|
|
93
221
|
- Do not retry a failed selector unchanged. The retry already happened inside
|
|
94
222
|
the tool — snapshot again and find out what the page really contains.
|