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 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 (this)
143
- - v0.5 — `act:` recovery via Stagehand, `slice.recovered` events
144
- - v0.6 — `wait_for_mfa` / `wait_for_email_otp` emitting `slice.paused` with
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.4.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wb-browser-runtime",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Browser sidecar runtime for wb — Browserbase + Playwright over the wb-sidecar/1 line-framed JSON protocol.",
5
5
  "bin": {
6
6
  "wb-browser-runtime": "bin/wb-browser-runtime.js"