wb-browser-runtime 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -3
- package/bin/wb-browser-runtime.js +62 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,10 +85,45 @@ example, see the `browserbase-hn-upvoted-probe` runbook in the xatabase repo.
|
|
|
85
85
|
| `extract` | — | `selector` (rows), `fields: { name → spec }` |
|
|
86
86
|
| `assert` | `assert: <selector>` | `selector`, `text_contains`, `url_contains` |
|
|
87
87
|
| `eval` | `eval: <js>` | `script` |
|
|
88
|
+
| `save` | `save: <name>` | `name`, `value` (captures prior `extract`/`eval` when omitted) |
|
|
88
89
|
|
|
89
90
|
`extract`'s `fields` entries are either a CSS selector string (returns
|
|
90
91
|
`textContent`), or `{ selector, attr }` to read an attribute.
|
|
91
92
|
|
|
93
|
+
## Artifacts
|
|
94
|
+
|
|
95
|
+
`wb` exports `$WB_ARTIFACTS_DIR` to every cell — a per-run directory
|
|
96
|
+
(`~/.wb/runs/<run_id>/artifacts/` by default) where any cell can drop files
|
|
97
|
+
that later cells will read back. The browser `save:` verb is the
|
|
98
|
+
sidecar-side equivalent:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
- extract:
|
|
102
|
+
selector: .order-row
|
|
103
|
+
fields:
|
|
104
|
+
id: .order-id
|
|
105
|
+
total: .total
|
|
106
|
+
- save: orders # writes $WB_ARTIFACTS_DIR/orders.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Forms:
|
|
110
|
+
|
|
111
|
+
- `save: <name>` — captures the previous verb's JSON output (from
|
|
112
|
+
`extract` or `eval`) into `<name>.json`.
|
|
113
|
+
- `save: { name: orders, value: { ... } }` — writes an inline value.
|
|
114
|
+
- `save: {}` — auto-names the file `cell-<block_index>-<rand>.json`.
|
|
115
|
+
|
|
116
|
+
Downstream bash/python cells read the file directly:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
jq '.[0].id' "$WB_ARTIFACTS_DIR/orders.json"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When `WB_ARTIFACTS_UPLOAD_URL` is set (template supports `{run_id}` and
|
|
123
|
+
`{filename}`), `wb` POSTs each new artifact file after the cell that
|
|
124
|
+
produced it completes. Auth reuses `WB_RECORDING_UPLOAD_SECRET`
|
|
125
|
+
(`Authorization: Bearer <…>`); failures are logged and non-fatal.
|
|
126
|
+
|
|
92
127
|
## Protocol
|
|
93
128
|
|
|
94
129
|
Line-framed JSON, one message per line, on stdin/stdout. `stderr` is treated as
|
|
@@ -139,7 +174,8 @@ Sidecar exits 0.
|
|
|
139
174
|
- v0.1 — protocol skeleton (echo only)
|
|
140
175
|
- v0.2 — `slice.session_started` event with stub URL
|
|
141
176
|
- v0.3 — Browserbase + playwright-core, real `goto/fill/click/wait_for/extract/assert`
|
|
142
|
-
- v0.4 — rrweb + CDP screencast recording, uploaded to a consumer endpoint
|
|
143
|
-
- v0.5 — `
|
|
144
|
-
- v0.6 — `
|
|
177
|
+
- v0.4 — rrweb + CDP screencast recording, uploaded to a consumer endpoint
|
|
178
|
+
- v0.5 — `save:` verb + shared `$WB_ARTIFACTS_DIR` for cross-cell data (this)
|
|
179
|
+
- v0.6 — `act:` recovery via Stagehand, `slice.recovered` events
|
|
180
|
+
- v0.7 — `wait_for_mfa` / `wait_for_email_otp` emitting `slice.paused` with
|
|
145
181
|
`resume_url`
|
|
@@ -41,10 +41,11 @@ const SUPPORTS = [
|
|
|
41
41
|
"extract",
|
|
42
42
|
"assert",
|
|
43
43
|
"eval",
|
|
44
|
+
"save",
|
|
44
45
|
];
|
|
45
46
|
|
|
46
47
|
const BB_BASE = "https://api.browserbase.com";
|
|
47
|
-
const VERSION = "0.
|
|
48
|
+
const VERSION = "0.5.0";
|
|
48
49
|
|
|
49
50
|
// --- Recording config -------------------------------------------------------
|
|
50
51
|
//
|
|
@@ -597,7 +598,7 @@ function arg(value, primaryKey) {
|
|
|
597
598
|
return {};
|
|
598
599
|
}
|
|
599
600
|
|
|
600
|
-
async function runVerb(page, verb, index) {
|
|
601
|
+
async function runVerb(page, verb, index, ctx) {
|
|
601
602
|
const name = verbName(verb);
|
|
602
603
|
const raw = verb[name];
|
|
603
604
|
const a = expand(arg(raw, defaultKey(name)));
|
|
@@ -670,6 +671,7 @@ async function runVerb(page, verb, index) {
|
|
|
670
671
|
// Emit as JSON to stdout so wb captures it in step.complete.stdout.
|
|
671
672
|
// Pretty-printed for readability when a runbook surfaces the output.
|
|
672
673
|
console.log(JSON.stringify(items, null, 2));
|
|
674
|
+
if (ctx) ctx.lastResult = items;
|
|
673
675
|
return `${rowSelector} → ${items.length} rows`;
|
|
674
676
|
}
|
|
675
677
|
case "assert": {
|
|
@@ -695,13 +697,63 @@ async function runVerb(page, verb, index) {
|
|
|
695
697
|
// Run arbitrary JS in the page; result is JSON-serialized to stdout.
|
|
696
698
|
const result = await page.evaluate(a.script);
|
|
697
699
|
console.log(JSON.stringify(result, null, 2));
|
|
700
|
+
if (ctx) ctx.lastResult = result;
|
|
698
701
|
return `script ran`;
|
|
699
702
|
}
|
|
703
|
+
case "save": {
|
|
704
|
+
// Persist a JSON artifact into $WB_ARTIFACTS_DIR so later cells can read
|
|
705
|
+
// it and wb can upload it. Captures the previous verb's output unless
|
|
706
|
+
// the author provides an explicit `value:`.
|
|
707
|
+
const artifactsDir = (process.env.WB_ARTIFACTS_DIR || "").trim();
|
|
708
|
+
if (!artifactsDir) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
"save: $WB_ARTIFACTS_DIR is not set — run this workbook via `wb run` (wb exports the dir for you)",
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
const explicitValue = a.value !== undefined;
|
|
714
|
+
const payload = explicitValue ? a.value : ctx?.lastResult;
|
|
715
|
+
if (payload === undefined) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
"save: no value provided and no prior extract/eval result to capture",
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const name =
|
|
721
|
+
typeof a.name === "string" && a.name.trim().length > 0
|
|
722
|
+
? sanitizeArtifactName(a.name)
|
|
723
|
+
: autoArtifactName(ctx?.blockIndex ?? index);
|
|
724
|
+
const filename = name.endsWith(".json") ? name : `${name}.json`;
|
|
725
|
+
const full = path.join(artifactsDir, filename);
|
|
726
|
+
await fsPromises.mkdir(artifactsDir, { recursive: true });
|
|
727
|
+
await fsPromises.writeFile(
|
|
728
|
+
full,
|
|
729
|
+
JSON.stringify(payload, null, 2),
|
|
730
|
+
"utf8",
|
|
731
|
+
);
|
|
732
|
+
send({
|
|
733
|
+
type: "slice.artifact_saved",
|
|
734
|
+
filename,
|
|
735
|
+
path: full,
|
|
736
|
+
bytes: Buffer.byteLength(JSON.stringify(payload)),
|
|
737
|
+
});
|
|
738
|
+
return `→ ${filename}`;
|
|
739
|
+
}
|
|
700
740
|
default:
|
|
701
741
|
throw new Error(`unsupported verb: ${name}`);
|
|
702
742
|
}
|
|
703
743
|
}
|
|
704
744
|
|
|
745
|
+
function sanitizeArtifactName(s) {
|
|
746
|
+
// Keep author-chosen names readable but safe as filenames. Drop anything
|
|
747
|
+
// that could escape the artifacts dir (slashes, NULs, etc.).
|
|
748
|
+
return String(s).replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 200);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function autoArtifactName(blockIndex) {
|
|
752
|
+
const rand = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
753
|
+
const n = Number.isFinite(blockIndex) ? blockIndex : 0;
|
|
754
|
+
return `cell-${n}-${rand}`;
|
|
755
|
+
}
|
|
756
|
+
|
|
705
757
|
function defaultKey(name) {
|
|
706
758
|
switch (name) {
|
|
707
759
|
case "goto":
|
|
@@ -716,6 +768,8 @@ function defaultKey(name) {
|
|
|
716
768
|
return "key";
|
|
717
769
|
case "eval":
|
|
718
770
|
return "script";
|
|
771
|
+
case "save":
|
|
772
|
+
return "name";
|
|
719
773
|
default:
|
|
720
774
|
return "value";
|
|
721
775
|
}
|
|
@@ -733,6 +787,8 @@ async function handleSlice(msg) {
|
|
|
733
787
|
const verbs = Array.isArray(msg.verbs) ? msg.verbs : [];
|
|
734
788
|
const sessionName = msg.session || "default";
|
|
735
789
|
const restore = msg.restore || null;
|
|
790
|
+
const blockIndex =
|
|
791
|
+
typeof msg.block_index === "number" ? msg.block_index : null;
|
|
736
792
|
|
|
737
793
|
let session;
|
|
738
794
|
try {
|
|
@@ -750,11 +806,14 @@ async function handleSlice(msg) {
|
|
|
750
806
|
// is where we'd jump to verbs[restore.state.verb_index].
|
|
751
807
|
const startAt = restore?.state?.verb_index ?? 0;
|
|
752
808
|
|
|
809
|
+
// Per-slice scratch so `save:` can capture the prior verb's JSON output.
|
|
810
|
+
const sliceCtx = { lastResult: undefined, blockIndex };
|
|
811
|
+
|
|
753
812
|
for (let i = startAt; i < verbs.length; i++) {
|
|
754
813
|
const v = verbs[i];
|
|
755
814
|
const name = verbName(v);
|
|
756
815
|
try {
|
|
757
|
-
const summary = await runVerb(session.page, v, i);
|
|
816
|
+
const summary = await runVerb(session.page, v, i, sliceCtx);
|
|
758
817
|
send({
|
|
759
818
|
type: "verb.complete",
|
|
760
819
|
verb: name,
|
package/package.json
CHANGED