@opencraw/mcp 0.1.45 → 0.1.47

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 (60) hide show
  1. package/README.md +27 -1
  2. package/dist/diff-tool/diff-tool.handler.d.ts +42 -0
  3. package/dist/diff-tool/index.d.ts +3 -0
  4. package/dist/docs-tool/docs-folder.repository.d.ts +24 -0
  5. package/dist/docs-tool/docs-root.client.d.ts +8 -0
  6. package/dist/docs-tool/docs-tool.handler.d.ts +21 -0
  7. package/dist/docs-tool/docs-topics.algorithm.d.ts +39 -0
  8. package/dist/docs-tool/index.d.ts +3 -0
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.esm.js +430 -1
  11. package/dist/list-tool/index.d.ts +3 -0
  12. package/dist/list-tool/list-recipes-tool.handler.d.ts +32 -0
  13. package/dist/main.d.ts +7 -0
  14. package/dist/probe-tool/index.d.ts +2 -0
  15. package/dist/probe-tool/probe-tool.handler.d.ts +27 -0
  16. package/dist/recipe-source/index.d.ts +3 -0
  17. package/dist/recipe-source/recipe-source.mapper.d.ts +21 -0
  18. package/dist/run-tool/index.d.ts +3 -0
  19. package/dist/run-tool/run-tool.handler.d.ts +51 -0
  20. package/dist/server/create-server.use-case.d.ts +11 -0
  21. package/dist/server/index.d.ts +2 -0
  22. package/dist/src/docs-tool/docs-folder.repository.d.ts +24 -0
  23. package/dist/src/docs-tool/docs-root.client.d.ts +8 -0
  24. package/dist/src/docs-tool/docs-tool.handler.d.ts +21 -0
  25. package/dist/src/docs-tool/docs-topics.algorithm.d.ts +39 -0
  26. package/dist/src/docs-tool/index.d.ts +3 -0
  27. package/dist/src/server/create-server.use-case.d.ts +1 -1
  28. package/dist/src/studio-tool/index.d.ts +2 -0
  29. package/dist/src/studio-tool/studio-command.client.d.ts +11 -0
  30. package/dist/src/studio-tool/studio-instance.repository.d.ts +22 -0
  31. package/dist/src/studio-tool/studio-tools.handler.d.ts +47 -0
  32. package/dist/studio-tool/index.d.ts +2 -0
  33. package/dist/studio-tool/studio-command.client.d.ts +11 -0
  34. package/dist/studio-tool/studio-instance.repository.d.ts +22 -0
  35. package/dist/studio-tool/studio-tools.handler.d.ts +47 -0
  36. package/dist/validate-tool/index.d.ts +3 -0
  37. package/dist/validate-tool/validate-tool.handler.d.ts +31 -0
  38. package/docs/how-it-works/01-recipes.md +698 -0
  39. package/docs/how-it-works/02-page-steps.md +672 -0
  40. package/docs/how-it-works/03-data-steps.md +813 -0
  41. package/docs/how-it-works/04-flow-steps.md +738 -0
  42. package/docs/how-it-works/05-templates-and-scope.md +428 -0
  43. package/docs/how-it-works/06-documents.md +1413 -0
  44. package/docs/how-it-works/07-mapping-and-transforms.md +699 -0
  45. package/docs/how-it-works/08-policies.md +405 -0
  46. package/docs/how-it-works/09-running.md +804 -0
  47. package/docs/how-it-works/README.md +225 -0
  48. package/docs/recipes/README.md +22 -0
  49. package/docs/recipes/access.md +440 -0
  50. package/docs/recipes/authoring.md +1503 -0
  51. package/docs/recipes/azure-durable.md +180 -0
  52. package/docs/recipes/captcha.md +204 -0
  53. package/docs/recipes/movies-example.md +12 -0
  54. package/docs/recipes/programmatic-authoring.md +101 -0
  55. package/docs/recipes/quick-guide.md +157 -0
  56. package/docs/recipes/vehicles-example.md +9 -0
  57. package/docs/recipes/worker-mode.md +221 -0
  58. package/docs/studio/manual.md +312 -0
  59. package/docs/studio/pdf-agreements.md +150 -0
  60. package/package.json +29 -4
@@ -0,0 +1,9 @@
1
+ <p align="center">
2
+ <img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
3
+ </p>
4
+
5
+ # Worked example: vehicle configurations
6
+
7
+ This write-up now lives with its example: [Every version and configuration of one model: BYD UK and Kia UK](../../examples/vehicles/README.md#how-the-recipes-were-written). Finding the data in script-heavy configurators, the BYD and Kia routes, and the trace.
8
+
9
+ Every example is listed in [examples/README.md](../../examples/README.md).
@@ -0,0 +1,221 @@
1
+ <p align="center">
2
+ <img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
3
+ </p>
4
+
5
+ # Worker mode: a pool of windows working through a queue
6
+
7
+ `crawler.run` crawls a recipe set and stops. **Worker mode** is for long crawls of one site with thousands of
8
+ items: one report per month × region × filter set, fed from a queue that another process fills. The pool:
9
+
10
+ - **keeps every window busy.** A window that finishes an item takes the next one at once. There are no
11
+ batches, and no window waits for the others;
12
+ - **keeps each window's page between items.** A window remembers what its page already shows (the report
13
+ type, the axes, the region picked) and an item only changes what differs, then submits;
14
+ - **matches the site's health.** It adds a window while items succeed and backs off when they fail. It never
15
+ takes a window away from an item it is running.
16
+
17
+ It is a restaurant floor. Waiters take the next order as soon as their hands are free, and leave the
18
+ cutlery on the table between guests. When the kitchen falls behind, the manager sends waiters home as they
19
+ finish their tables. The manager never pulls a plate from under a guest.
20
+
21
+ ## 1. The recipe
22
+
23
+ A worker recipe is an ordinary input recipe. Two additions tell the window what it may keep:
24
+
25
+ ```json
26
+ {
27
+ "kind": "input", "id": "report", "output": "row", "mode": "web",
28
+ "vars": { "state": "DL", "rto": "", "group": "" },
29
+ "start": [{ "url": "https://stats.example.gov/report" }],
30
+ "window": { "check": "#axis", "maxItems": 200 },
31
+ "steps": [
32
+ { "type": "goto", "url": "{{ start.url }}", "keep": true },
33
+ { "type": "select", "selector": "#axis", "value": "maker", "keep": true },
34
+ { "type": "select", "selector": "#state", "values": ["{{ vars.state }}"], "force": true, "keep": true },
35
+ { "type": "select", "selector": "#rto", "values": ["{{ split(vars.rto) }}"], "force": true, "clear": true, "keep": true },
36
+ { "type": "select", "selector": "#group", "values": ["{{ split(vars.group) }}"], "force": true, "clear": true, "keep": true },
37
+ { "type": "click", "selector": "#apply" },
38
+ { "type": "wait", "selector": "#result .row" },
39
+ { "type": "extract", "id": "rows", "selector": "#result .row", "kind": "css", "many": true },
40
+ { "type": "forEach", "over": "rows", "as": "row", "emit": true, "steps": [] }
41
+ ],
42
+ "mapping": { "row": { "from": "row" } }
43
+ }
44
+ ```
45
+
46
+ **`keep: true`** marks a page action the window may skip:
47
+ - The window remembers each kept step as it last ran it, with every template rendered (`#state` set to `DL`).
48
+ - On the next item, a kept step that would do the same is skipped (`step:kept` in the trace).
49
+ - A kept step that runs again, because its value changed, makes the window forget **the kept steps after it**.
50
+ A change cascades: a new state reloads the RTO list, so the RTO pick must run again even when its value is
51
+ the one the window holds.
52
+ - Keep is for top-level page actions only (`goto`, `click`, `fill`, `press`, `select`, `scroll`, `wait`,
53
+ `evaluate`), and never on a step with an `id`: a skipped step binds nothing, so its value would be unset
54
+ for the next item. Loading refuses both.
55
+ - Steps without `keep` (Apply, the wait, the extract) run for every item.
56
+
57
+ **`clear: true`** on a `select` is not optional on a reused page. A blank filter var normally leaves the
58
+ control alone. On a reused page, the control would then keep the last item's pick, and item 2 ("every
59
+ group") would silently report item 1's group. With `clear`, blank values empty the control.
60
+
61
+ **`window.check`** is an element a reused page must still show before an item runs on it. A session that
62
+ expired or a page that went elsewhere fails the check, and the window is replaced instead of failing the
63
+ item. **`window.maxItems`** replaces a window after that many items anyway: memory, and server state that
64
+ grows stale.
65
+
66
+ The same recipe runs as it is with `crawler.run` or the CLI: without a window, `keep` changes nothing.
67
+
68
+ ## 2. The work source
69
+
70
+ The pool asks a **work source** for items, from several windows at once, and tells it how each ended:
71
+
72
+ ```ts
73
+ interface WorkItem { id: string, vars: Record<string, string | number | boolean>, recipe?: string }
74
+ interface WorkSource {
75
+ next (options?: { signal }): Promise<WorkItem | undefined> // may wait (a refill); undefined = no more work
76
+ done? (item, report, records): Promise<void> // succeeded: its records are written, and given here too
77
+ failed? (item, report, outcome): Promise<void> // 'failure' or 'neutral'; nothing of it was written
78
+ }
79
+ ```
80
+
81
+ - `workFrom(items)` turns a plain list into a source.
82
+ - A real queue (a table with claims and leases, priorities, a local buffer refilled when it runs low) is
83
+ the source's business. `next` returns from the buffer and refills it; `done` marks the item consumed;
84
+ `failed` counts a retry (`failure`) or puts it back as it was (`neutral`).
85
+ - `item.id` is on every event of the item, on its report (`item`) and on its records (`record.source.item`).
86
+ - `item.recipe` picks the input recipe when the set has several.
87
+
88
+ ### Pushing items: the inbox
89
+
90
+ Some callers don't own a queue: they receive items one at a time (an HTTP request each, a message handler)
91
+ and want each item's result back where they asked for it. `createWorkInbox()` turns pushes into a source:
92
+
93
+ ```ts
94
+ import { createWorkInbox } from '@opencraw/core'
95
+
96
+ const inbox = createWorkInbox()
97
+ const working = crawler.work(recipes, inbox.source, { windows: { min: 1, max: 8 } })
98
+ working.catch(error => inbox.abort(error)) // work itself failed: reject what is still waiting
99
+
100
+ const result = await inbox.submit({ id: 'r-42', vars: { state: 'DL' } })
101
+ // { outcome: 'success', report, records } | { outcome: 'failure' | 'neutral', report }
102
+
103
+ inbox.close() // no new items; queued and running ones finish
104
+ const report = await working // then work() ends with its report
105
+ ```
106
+
107
+ - **Items first come, first served; windows last come, first served.** The window that just finished takes
108
+ the next item while its page is warm. One that has waited long stays idle and, with `windows.idle`, retires.
109
+ - **One result per item.** `submit` resolves when that item ends, with its own records only. `neutral` is
110
+ returned, not retried: the caller decides whether to submit it again.
111
+ - **No double runs.** Submitting an id that is queued or running returns that run's result. Once it has
112
+ ended, the same id runs again.
113
+ - **Demand.** `inbox.idle` counts windows waiting for an item, `inbox.queued` items waiting for a window,
114
+ `inbox.running` items in progress. Keep submitting while `idle` is above zero, or the pool can't grow: it
115
+ only keeps as many windows busy as the caller keeps items in flight.
116
+ - **Cancel before start.** `submit(item, { signal })` takes the item back while it is queued; a running item
117
+ is not interrupted.
118
+ - `close()` refuses new items; `abort(reason)` also rejects every item not finished yet.
119
+
120
+ **Records are held until the item succeeds.** Mapped records wait in memory. De-duplication, `resume` and
121
+ the sink see them only when the whole item succeeded. A failed item therefore writes nothing and leaves no
122
+ key behind, so running it again later is safe. `done` receives the records, for a source that files one
123
+ document per item.
124
+
125
+ **De-duplication is per item.** A key seen twice within one item is a duplicate. The same key in another item
126
+ is that item's record: two state reports that both list the same maker each keep their row. This is the
127
+ `recipe` scope, since each item is one run of its recipe. Pass `dedupe: 'run'` to `createCrawler` to drop a
128
+ key for the rest of the `work` call, or `'off'` to keep everything.
129
+
130
+ ## 3. Running it
131
+
132
+ ```ts
133
+ import { createCrawler, loadRecipeSet, jsonLinesSink } from '@opencraw/core'
134
+
135
+ const recipes = await loadRecipeSet({ output: 'row.output.json', inputs: ['report.input.json'] })
136
+ const crawler = createCrawler({ sink: jsonLinesSink('rows.jsonl'), captchaSolvers: [reader], onEvent })
137
+ try {
138
+ const report = await crawler.work(recipes, source, {
139
+ windows: { min: 5, max: 20, start: 12, grow: { after: 10 }, shrink: 'one', restart: { after: 20 } },
140
+ })
141
+ // report: { items: { success, failure, neutral }, records, windows: { start, peak, final }, restarts, sink, durationMs }
142
+ } finally {
143
+ await crawler.close()
144
+ }
145
+ ```
146
+
147
+ One `run` or `work` at a time per crawler: they share its sink.
148
+
149
+ ## 4. How many windows
150
+
151
+ | Setting | Default | What it does |
152
+ |---|---|---|
153
+ | `windows: 4` | 1 | A fixed pool of four. |
154
+ | `min`, `max` | 1, `min` (or `start` when higher) | The bounds. |
155
+ | `start` | `min` | Where the pool starts. |
156
+ | `grow.after` | 10 | One more window after this many successes in a row, counted only while every window is busy: with a window waiting for work, another would only wait too. |
157
+ | `shrink` | `half` | On a failure: half the windows (`half`) or one fewer (`one`). |
158
+ | `restart.after` | never | After this many failures in a row, the browser is relaunched and the pool goes back to `min`. |
159
+ | `idle.afterMs` | never | A window that has waited this long for an item retires, never below `min`. For push-fed sources (the inbox), whose callers slow down. |
160
+
161
+ What makes the pool safe to leave running overnight:
162
+
163
+ - **A window leaves only between items.** A shrink lowers the number the pool aims for. The windows above it
164
+ leave as they finish their current item; the window whose item failed is usually the first. No item is
165
+ ever cut off by a shrink, so one failure never causes another.
166
+ - **One bad moment shrinks the pool once.** When the site hiccups, every item in flight fails at about the
167
+ same time. Only failures of items that started *after* the last shrink shrink it again. The others count
168
+ towards a restart, but the shrink has already answered them. TCP treats a burst of losses the same way.
169
+ - **A failed item gets a fresh window.** Its page is in a state nobody knows: the window is closed and the
170
+ lane opens a new one for its next item (`window:open fresh`).
171
+ - **A restart waits for every running item to finish.** New items wait for the relaunch; windows opened in
172
+ the old browser are replaced when their next item comes. After a restart the pool is back at `min` and
173
+ grows again from there.
174
+ - **An idle window goes, then comes back when needed.** With `idle.afterMs`, a window that waits that long for
175
+ an item retires through the same path as a shrink (`window:close idle`, `windows:change`), never below
176
+ `min`. Successes grow the pool again. The pool passes `next` a signal that aborts when the window retires;
177
+ the inbox takes the wait back. A source that can't may still return an item for it later: that item gets a
178
+ window of its own (`window:open late`), so nothing is lost.
179
+ - **A browser that dies is relaunched.** Items that were running end as `neutral` (the source gets them back
180
+ untouched), and the next window launches a new browser.
181
+
182
+ **How an item ended** (`classify`, overridable):
183
+
184
+ | Outcome | Default for | Effect |
185
+ |---|---|---|
186
+ | `success` | no error | Counts towards growing; records written; `done`. |
187
+ | `failure` | a block, retries used up (HTTP errors, timeouts, connection failures), a step that failed | Shrinks the pool; fresh window; `failed(…, 'failure')`. |
188
+ | `neutral` | a captcha the solver could not get past; the browser closed under the item | The pool does not move; fresh window; `failed(…, 'neutral')`: put it back as it was. |
189
+
190
+ `report.errorKind` says why a run stopped (`captcha`, `blocked`, `browser`, `http`, `timeout`, `network`,
191
+ `step`, `mapping`, `host`, `error`). Write your own `classify` from it, for instance to treat `step` failures (a
192
+ selector that matched nothing: a recipe problem, not the site's health) as `neutral`.
193
+
194
+ ## 5. Watching it
195
+
196
+ Every event of a window carries `window` and `item`, and `traceLine` prefixes its lines with `[w3]`:
197
+
198
+ ```text
199
+ [w1] ⧉ window opened (start)
200
+ [w1] ▶ report [state=DL, rto=DL53, group=Bus] (web)
201
+ [w1] ≡ steps.0 goto kept
202
+ [w1] ≡ steps.1 select kept
203
+ [w1] · steps.3 select 812 ms
204
+ [w1] ■ report [state=DL, rto=DL53, group=Bus]: 12 emitted, 0 rejected, 0 duplicates, 1 pages, 4213 ms
205
+ [w1] ✓ item DL-DL53-Bus success, 4214 ms
206
+ [w1] ⇅ windows 12 → 13: 10 successes in a row
207
+ [w13] ⧉ window opened (grow)
208
+ [w4] ✖ item GA-GA1-Car failure, 30211 ms: step steps.6 (wait) failed: Timeout 30000ms exceeded
209
+ [w4] ⇅ windows 13 → 12: an item failed
210
+ [w4] ⧉ window closed (fresh)
211
+ [w9] ⧉ window closed (retire)
212
+ ```
213
+
214
+ ## 6. Limits
215
+
216
+ - Kept steps are the top-level list's. A page whose state cannot be read back from what the recipe did
217
+ (a wizard that moves forward only) is not a good fit: leave `keep` off and each item redoes its steps on
218
+ the same window, which still saves the context and the browser start.
219
+ - A window runs one recipe. An item for another recipe of the set replaces the window.
220
+ - The pool lives in one process. Several machines each run their own pool; claiming items so that no two
221
+ machines take the same one is the source's job.
@@ -0,0 +1,312 @@
1
+ <p align="center">
2
+ <img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
3
+ </p>
4
+
5
+ # The OpenCraw Studio manual
6
+
7
+ Studio is a visual editor for recipes: open a site or a document, click the data you want, and get a
8
+ recipe that runs. This guide builds one, start to finish — [books.toscrape.com](https://books.toscrape.com),
9
+ the same site [How OpenCraw works](../how-it-works/README.md) walks through, a site built for scraping
10
+ practice — without hand-writing a line of JSON. By the end you will have a working `input`/`output` recipe
11
+ pair and know your way around every tab.
12
+
13
+ If you would rather write recipes by hand, [`docs/recipes/authoring.md`](../recipes/authoring.md) is the
14
+ full reference; this guide is the other way in.
15
+
16
+ ## Contents
17
+
18
+ 1. [Install and start](#1-install-and-start)
19
+ 2. [Opening a workspace](#2-opening-a-workspace)
20
+ 3. [The shell](#3-the-shell)
21
+ 4. [The Steps tab: picking on a live page](#4-the-steps-tab-picking-on-a-live-page)
22
+ 5. [The Record tab: fields and mapping](#5-the-record-tab-fields-and-mapping)
23
+ 6. [Running a sample](#6-running-a-sample)
24
+ 7. [The Inspect panel: data that isn't on the page](#7-the-inspect-panel-data-that-isnt-on-the-page)
25
+ 8. [Document canvases](#8-document-canvases)
26
+ 9. [Recording a login](#9-recording-a-login)
27
+ 10. [The JSON tab](#10-the-json-tab)
28
+ 11. [Tips and troubleshooting](#11-tips-and-troubleshooting)
29
+
30
+ ## 1. Install and start
31
+
32
+ Studio is its own package, kept separate from the engine so a plain crawl never pulls in a browser UI:
33
+
34
+ ```sh
35
+ npm install -g @opencraw/cli @opencraw/studio
36
+ ```
37
+
38
+ Start it on a folder of recipes (or an empty folder — see [§2](#2-opening-a-workspace)):
39
+
40
+ ```sh
41
+ opencraw studio recipes/
42
+ # or, without the cli:
43
+ npx @opencraw/studio recipes/
44
+ ```
45
+
46
+ Either prints the URL to open, with a random access token already in it:
47
+
48
+ ```text
49
+ OpenCraw Studio: http://127.0.0.1:52341/?token=8f2a1c9e4b7d3f6a0e5c8b1d2a4f7e9c
50
+ Workspace: /home/you/project/recipes
51
+ ```
52
+
53
+ Open that URL in a browser. The server only listens on `127.0.0.1` and only answers requests carrying that
54
+ token, so nothing else on your machine (or network) can reach it — see
55
+ [the plan's §8](../research/recipe-studio.md#8-security) for the full list of what it does and doesn't do.
56
+ Leave the terminal running; `Ctrl+C` stops the server. If you started it with `opencraw studio` and get
57
+ "the studio is not installed", `npm install @opencraw/studio` — the cli keeps it as an optional dependency
58
+ so a plain crawl install stays light.
59
+
60
+ ## 2. Opening a workspace
61
+
62
+ The toolbar has three jobs, and each has one place:
63
+
64
+ | Button | Question it answers | What is in it |
65
+ |---|---|---|
66
+ | **Recipe ▾** | Which recipe am I editing? | the recipes in the open folder, **New recipe…**, **Open folder…** |
67
+ | **Load ▾** | What does it read? | **Document…**, **Web page (URL)…** |
68
+ | **Run sample** / **Stop** | Does it work? | runs the open recipe |
69
+
70
+ Next to them, **All changes saved** says the edits you make are written as you make them (the Steps tab, loading, creating a
71
+ recipe); it reads **Saving…** while a write is on its way. The **Record** and **JSON** tabs keep their edits
72
+ as a draft until you press their own **Save**, so a change there is not written before that.
73
+
74
+ **A recipe is an input and its output.** The input recipe (`<id>.ocw-in.json`) names its output recipe
75
+ (`<id>.ocw-out.json`, by the input's `output` id). Opening a recipe from **Recipe ▾** opens both: Steps edits the
76
+ input, Record edits the output and the mapping between them. You never open the pair separately.
77
+
78
+ **Opening a folder.** **Recipe ▾ → Open folder…** takes any folder — one that already has `.ocw-in.json` /
79
+ `.ocw-out.json` files in it, or a brand new one. It opens the operating system's folder dialog (the desktop app's native one, or the VS Code extension's own
80
+ dialog when Studio runs from there); on a machine with no dialog, a box to type the path in appears instead. Starting Studio
81
+ with `opencraw studio <folder>` opens that folder already.
82
+
83
+ **Starting from nothing?** **Recipe ▾ → New recipe…**: type an id (lowercase letters, digits and hyphens — the
84
+ same pattern every recipe id follows), optionally a start URL, and **Create**. That writes a minimal, valid
85
+ pair — `<id>.ocw-in.json` and `<id>.ocw-out.json` — into the open folder and selects it: a `web` recipe with one
86
+ `goto` step, an `emit`, and no fields mapped yet. Everything from here builds on top of that.
87
+
88
+ **Loading what it reads.** A page or a document is the sample a recipe is built against, so **Load ▾** puts it
89
+ into the recipe that is open:
90
+
91
+ - **Web page (URL)…** with a web recipe open replaces its start page; the steps and the mapping stay.
92
+ - **Document…** (or dropping a PDF, spreadsheet, CSV, PowerPoint, Word, Markdown, JSON, YAML or XML file
93
+ anywhere on the window) with a document recipe open replaces the file it reads. The studio copies the file
94
+ into the workspace folder, next to the recipes, so the folder stays self-contained.
95
+ - If the open recipe reads the other kind of thing — a document into a web recipe, or a page into a document
96
+ recipe — or no recipe is open, Load starts a new recipe instead and leaves the open one untouched. A bar
97
+ under the toolbar says which happened. New recipes are named after the file (`Q3 Report.pdf` → `q3-report`;
98
+ a second one becomes `q3-report-2`) or the page's host, and a document opens on its own canvas
99
+ ([§8](#8-document-canvases)).
100
+ - No folder open yet? Load needs one to write into: open one first.
101
+
102
+ For this guide's example, open any folder and create a recipe with the id `books`.
103
+
104
+ ## 3. The shell
105
+
106
+ The window splits into three: the **content pane** on the left shows what the engine actually sees at the
107
+ recipe's start point (not a live, interactive copy of the site — see [§4](#4-the-steps-tab-picking-on-a-live-page)
108
+ for why); the **editor pane** on the right has three tabs, **Steps**, **Record** and **JSON**; a **preview
109
+ strip** runs along the bottom with the last sample's **Records**, its **Trace**, and (once something is
110
+ missing) a **Why?** explanation. Both the content/editor split and the workspace/preview split are
111
+ draggable.
112
+
113
+ The toolbar's recipe picker lists every `input` recipe in the open folder; **Run sample** crawls the
114
+ selected one with a small budget (a handful of pages and records) and streams the result into the preview
115
+ strip, live.
116
+
117
+ ## 4. The Steps tab: picking on a live page
118
+
119
+ Open the **Steps** tab. A new recipe has one card: **Go to `https://example.com`** (the placeholder start
120
+ URL — edit its address field to `https://books.toscrape.com/catalogue/category/books/mystery_3/index.html`,
121
+ the Mystery category). A second card, **Emit**, closes the loop for now; you'll move it once there's
122
+ something to loop over.
123
+
124
+ The content pane does not load the live site — it loads a **snapshot the engine itself took** by running
125
+ the recipe up to that step (a real Playwright page for a `web` recipe, a real HTTP fetch for `api`), with
126
+ scripts stripped and every element marked with a stable id. That is deliberate: it is exactly what the
127
+ engine will read, so a pick here is a pick the engine reproduces exactly, byte for byte, every time it
128
+ runs — not a "close enough" guess at what you saw in a live browser.
129
+
130
+ Click **Read** above the content pane to enter pick mode, then hover over a book's title in the page. The
131
+ element under the cursor outlines, with its candidate CSS selector shown. Click it: a **Read** card appears
132
+ in the Steps outline — *Read `title` from `.product_pod h3 a` (attr: title)*.
133
+
134
+ Now click a **second** book's title. Studio walks up from both clicked elements to the nearest ancestors
135
+ that repeat with the same tag and class (`article.product_pod`), and rewrites the two single reads into a
136
+ safe loop by construction: an `extract` of every matching item, a `forEach` over them, and the field read
137
+ `from` each item — never the "wrapper trap" a hand-written selector can fall into (an ancestor that isn't
138
+ actually one-per-item). The outline now shows **Read `books`... → For each `book` in `books` → Read `title`
139
+ from `book`**.
140
+
141
+ Repeat picking inside that loop for a **price** (`.price_color`) and an **availability** (`.availability`)
142
+ on the same two books, each producing another **Read** card nested in the same `forEach`. Move the **Emit**
143
+ card (drag, or the `+` menu's own **Emit**) inside the loop, after the reads, so one record comes out per
144
+ book instead of one for the whole page.
145
+
146
+ A card's **pill** — the small coloured `books`, `book`, `title` labels — is the id the next card downstream
147
+ can read `from`. Hovering a card highlights every matching element in the content pane with a count, so you
148
+ can confirm a selector actually matches what you expect before moving on. The document canvases do the same
149
+ in both directions: a tree node, a region of a PDF page or a slide, a chart, or a cell a step already reads
150
+ is marked with the step's id (a dashed box, a pill), hovering the card fills it in, and hovering the marked
151
+ node, line, text box, chart or cell lights the card up. Container steps (`forEach`,
152
+ `paginate`, `if`) draw as brackets around their children: a card can only use a pill from its own bracket or
153
+ an outer one, so the outline can never let you build a binding error the loader would later reject.
154
+
155
+ The `+` row between any two cards (or at the end of a bracket) offers every step kind: **Go to**, **Read**,
156
+ **Loop**, **Next page**, **If**, **Fill form**, **Click**, **Set**, **Collect**, **Emit**. **Next page**
157
+ wraps everything already in the outline in a `paginate` bracket around a "next" link you then pick the same
158
+ way — turn it into one now around the whole loop, pointing at the category page's "next" link, and every
159
+ page of Mystery books feeds the same reads.
160
+
161
+ Every edit here — a pick, a move, a reorder — writes straight to the recipe's JSON file and re-validates
162
+ against the server's real loader (the same one `opencraw validate` uses): a binding problem shows on the
163
+ card that causes it, in place, not as a wall of text elsewhere.
164
+
165
+ **Hidden elements**: the **Show hidden** switch reveals (greyed) anything the snapshot marked as hidden on
166
+ the live page (`display: none`, zero size) — some sites keep a machine-readable value in a hidden node or a
167
+ `<meta>` tag that is easier to read than the rendered text.
168
+
169
+ ## 5. The Record tab: fields and mapping
170
+
171
+ Switch to the **Record** tab. This is the paired **output** recipe: the fields a record actually has, each
172
+ with a type, and the input recipe's **mapping** — which pill each field reads from, and what happens to the
173
+ value on the way in.
174
+
175
+ A strip at the top of the tab lists every value the steps read that no field uses yet, one chip each:
176
+ the pill, the field it would become and a guessed type (`price` becomes a `number`, `availability` a
177
+ `boolean`, a link taken from `href` a `url` made absolute; hover a chip for why). Click a chip's **+** to add
178
+ the field **and** its mapping at once, or **Add all**. Nothing is guessed silently: change a type or a name
179
+ afterwards like any field.
180
+
181
+ A recipe with output fields but no **Emit** step reads values and returns no records, so the tab says so and
182
+ offers **Add Emit step**.
183
+
184
+ Each field row opens with a one-line summary: a coloured **type** badge, the **pill** it reads from (or
185
+ *not mapped*, *generated*) and, once a sample has run, the **value** the selected record got.
186
+
187
+ **Drag to build the output.** Anything you can see can be dropped on the field list to become a field with its
188
+ mapping, typed the way its proposal would be:
189
+
190
+ - a **pill** from a **Steps** card: grab it and hold it over the **Record** tab (it opens after a moment),
191
+ then drop it on the list. Dropped on a field's own source box instead, it only maps that field;
192
+ - a **selection** staged in the content pane (a cell, a text box, a region of a PDF, a node of the page): drag
193
+ its chip onto the **Record** tab and the list. The studio adds the step to the recipe, as dropping it on the
194
+ Steps tab does, and the value it reads appears as a new field. The new field is a draft until you **Save**.
195
+
196
+ You can still add a field by hand with **+ field** and drag a **Steps** pill onto its row (or type the pill
197
+ name directly). Mark `title` as the record's **key**, so a duplicate never gets emitted twice.
198
+
199
+ Each field's **transform chain** is a row of small blocks — `trim`, `currency`, `boolean`, and so on
200
+ (the same closed set [§5.1 of the authoring guide](../recipes/authoring.md#51-transforms) documents) — add
201
+ one by clicking the row's own `+`. After a sample has run (see [§6](#6-running-a-sample)), each block
202
+ shows the **real value** at that point, taken from the last run's mapping trace: read `"£47.82"`, after
203
+ `currency` → `{"amount":47.82,"currency":"GBP"}`. A transform that fails on a real value turns red with the
204
+ engine's own reason, right on the block that failed — no guessing which one from a stack trace.
205
+
206
+ ## 6. Running a sample
207
+
208
+ Click **Run sample** in the toolbar. This is not a mocked preview: it is the same engine, in the same
209
+ process, running for real against a small budget (a few pages, a handful of records) so it finishes fast
210
+ while you're editing. The **Records** panel at the bottom fills in as they arrive — toggle between a
211
+ **Table** and a compact **JSONL** view, export what you see (JSON/JSONL/CSV), or **Enlarge** it into a full
212
+ window. The **Trace** panel streams the engine's own `traceLine` output live, one line per step, indented by
213
+ nesting — the exact same trace `opencraw run --trace` prints from the terminal.
214
+
215
+ Click an empty cell in the Records table (a field that came out missing) to get **Why?**: a plain-language
216
+ explanation built from the run's own events — which read matched nothing, on which item, and what
217
+ selectors were actually present nearby to try instead. **Stop** ends a run early; recipes with a start point
218
+ that changes each run (pagination, matrix variants) can be safely stopped mid-way without leaving anything
219
+ half-written, since nothing is saved until you click Save.
220
+
221
+ ## 7. The Inspect panel: data that isn't on the page
222
+
223
+ Not everything worth reading is visible text. Click **Inspect** above the content pane to open a DOM tree of
224
+ the same snapshot, alongside the rendered view — a browser inspector, but of exactly what the engine reads,
225
+ with hidden nodes greyed the same way the content pane marks them.
226
+
227
+ Two more tabs live here:
228
+
229
+ - **Data in the page**: `<script type="application/ld+json">` blocks, inline JSON state
230
+ (`window.__STATE__ = …`), and `<meta>` tags, listed apart from the DOM tree — click one to pick it as a
231
+ `regex` or `jsonpath` read, often more reliable than a CSS selector on rendered text that a redesign could
232
+ break.
233
+ - **Responses seen**: every JSON response the page fetched while it rendered (the same findings
234
+ `opencraw probe` reports from the terminal). Picking one switches the recipe from `web` mode to `api`
235
+ mode on that endpoint directly — usually faster and steadier than driving a browser at all, when a site's
236
+ data already arrives as clean JSON.
237
+
238
+ Clicking a node in either the DOM tree or a finding is the same pick a click in the content pane would be:
239
+ it lands the same kind of **Read** card in the Steps outline.
240
+
241
+ ## 8. Document canvases
242
+
243
+ The editor pane never changes — Steps, Record and JSON work the same regardless of source. Only the content
244
+ pane's **canvas** changes, to fit what it's actually showing:
245
+
246
+ | Source | Canvas | A click gives you |
247
+ |---|---|---|
248
+ | A web page, or Word/Markdown (read as HTML) | the snapshot iframe from [§4](#4-the-steps-tab-picking-on-a-live-page) | a CSS selector |
249
+ | JSON, YAML, XML, JSON Lines | a tree — click a value to read it, `[*]` reads every item of a list | a `jsonpath` (or `xpath` for XML); a "next" value offers the `paginate` cursor |
250
+ | PDF | the page rendered with the engine's own detected cells and rows drawn over it | in **Text** mode (the default) the line under the mouse snaps; a click stages it (a wrapped paragraph is staged whole, and a line under a heading stages everything between that heading and the next, as a `section` extract: the chip says what it is tied to, and a box you draw says it is a fixed position that breaks when the layout moves), shift+click extends to another line, dragging stages the box drawn — a chip shows exactly what the engine reads there, with **Add to recipe** (a `region` extract: `page=1 x=72..252 y=640..664`; a `Label: value` line adds a `regex` extract named after its label that reads the value only, wherever it sits on the page) or drag the chip onto the Steps tab. The page is fitted to the pane's width. A worked example, with every step pictured and a narrated video, is [One recipe for two PDFs of one layout](./pdf-agreements.md). In the table modes, the header row gives `selector`, the last row `until`, a column its name |
251
+ | Excel, CSV | a grid: sheet tabs, hidden sheets/rows marked, merged cells shown as one | in **Cell** mode (the default) the cell under the mouse lights up; a click stages it, shift+click (or press on one cell and release on another) stages the rectangle between — the chip shows the values, with **Add to recipe** (a `jsonpath` into the sheet by name and the cell by position: `$.sheets[?(@.name=='Prices')].rows[0][0]`, a rectangle a list) or drag it onto the Steps tab. In the table modes a sheet tab gives `sheet`, the header row `headerRows`, a merged group `fillDown` |
252
+ | PowerPoint | each slide redrawn from its shapes, with tables and charts listed apart | in **Text** mode (the default) the text box under the mouse snaps; a click stages it, shift+click extends, dragging stages the box drawn — the same chip as the PDF's, with **Add to recipe** (a `region` extract: `slide=3 x=60..900 y=30..90`, points from the slide's top-left corner) or drag it onto the Steps tab. In the table modes a table gives `table`/`slide`; a chart a `jsonpath` into its series |
253
+
254
+ Every one of these writes the matching `extract` step the same way a page pick does — the outline, the
255
+ Record tab and the JSON tab don't know or care which canvas produced it.
256
+
257
+ ## 9. Recording a login
258
+
259
+ Some data sits behind a login, or needs a click, a search box filled in, or an infinite-scroll load — none
260
+ of which a static snapshot can do. Click **Record** above the content pane: a real, headed browser window
261
+ opens for you to drive by hand. A banner across the top tracks what you've done as you go; every click,
262
+ fill, select and key press you make in that window becomes a step, live.
263
+
264
+ When you're done, **Stop recording**. Two ways to keep what you did:
265
+
266
+ - **Make this the login**: moves the recorded steps into the recipe's `session.bootstrap` — they run once,
267
+ before the crawl proper, to obtain cookies the rest of the recipe reuses. Use this for an actual sign-in.
268
+ - **Keep as steps**: appends them to the recipe's own `steps`, in place, the same as if you'd built them
269
+ with the Steps tab's `+` menu. Use this for something that's part of the crawl itself, not a one-time
270
+ setup (a search box, an infinite-scroll trigger).
271
+
272
+ **Discard** throws the recording away. Either way, the content pane's snapshot is retaken after the
273
+ recording closes, so the next pick reflects whatever state the page ended up in.
274
+
275
+ ## 10. The JSON tab
276
+
277
+ The recipe's file, live, in a real code editor (syntax highlighting, multi-cursor with `Ctrl`/`Cmd`-`D`,
278
+ search with `Ctrl`/`Cmd`-`F`) rather than a plain text box — with the server's own validation, from the same
279
+ loader `opencraw validate` uses, shown as inline markers at the exact spot each issue is about. Anything the
280
+ Steps/Record tabs cannot yet express (a construct they don't have a card for) still round-trips here as
281
+ readable JSON — nothing is ever silently dropped for being "too advanced" for the visual editors. Edit here
282
+ and the other tabs pick it up on the next save; edit there and this tab shows exactly what got written,
283
+ so the two never drift out of sync.
284
+
285
+ ## 11. Tips and troubleshooting
286
+
287
+ - **"Playwright's Chromium is not downloaded yet"**: the first time a `web` recipe needs a browser and none
288
+ is installed, the studio installs one for you automatically and retries — you'll see a line about it in
289
+ the terminal the studio is running in. If it can't (offline, no permission), it says so with the exact
290
+ command to run yourself: `npx playwright install chromium`.
291
+ - **A pick lands in the wrong place**: a pick always adds to the *end* of the recipe's top-level steps, not
292
+ inside whichever bracket happens to be open in the Steps tab — drag the new card into place afterward, or
293
+ build the loop first (as in [§4](#4-the-steps-tab-picking-on-a-live-page)) and pick from inside it.
294
+ - **Nothing to pick from**: an empty response, a page that needs JavaScript the snapshot didn't wait for, or
295
+ a login wall are all signs to reach for [§7](#7-the-inspect-panel-data-that-isnt-on-the-page)'s "responses
296
+ seen" tab or [§9](#9-recording-a-login)'s recorder before assuming the data isn't reachable at all.
297
+ - **The recipe files are the truth.** Nothing the studio does is hidden state: close it, and every pick,
298
+ every mapping, every transform is sitting in the JSON files exactly as saved. `opencraw validate` and
299
+ `opencraw run` work on them the same as any hand-written recipe.
300
+
301
+ ## 12. Letting an agent drive Studio
302
+
303
+ An AI agent connected to OpenCraw's MCP server (see the VS Code extension's *AI agent* view) can work in the Studio you have open, so you watch it
304
+ happen instead of reading a report:
305
+
306
+ - **`studio_status`** tells it whether Studio is running, which folder it has open and what it is sampling.
307
+ - **`studio_open_recipe`** opens a recipe, a folder or a document in your Studio, as if you had followed a link to it.
308
+ - **`studio_run_sample`** runs an input recipe as a sample: the records arrive in your Records tab, and the agent gets the counts and the first
309
+ records back.
310
+
311
+ Nothing needs setting up: a running Studio announces itself in `~/.opencraw/studio` (its address and its access token, readable only by you), and the
312
+ agent finds it there. Studio still listens on this machine only. Clicking a value on a page or document stays your click: the agent tells you where.