← back to Sublease Agentabrams
Browserbase crawler for CBRE/NAI (consent-filter, nested-address, poll-for-results); recorded 0-yield runs in crawl_runs. Spend $0.73, no usable inventory captured
0276f069eb446a01d39c296950d11ea55c32a91e · 2026-07-20 13:21:24 -0700 · Steve
Files touched
A crawl/bb-crawl.jsM package-lock.jsonM package.json
Diff
commit 0276f069eb446a01d39c296950d11ea55c32a91e
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 20 13:21:24 2026 -0700
Browserbase crawler for CBRE/NAI (consent-filter, nested-address, poll-for-results); recorded 0-yield runs in crawl_runs. Spend $0.73, no usable inventory captured
---
crawl/bb-crawl.js | 165 ++++++++++++++++++++++++++++++++++++++
package-lock.json | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 3 +-
3 files changed, 398 insertions(+), 1 deletion(-)
diff --git a/crawl/bb-crawl.js b/crawl/bb-crawl.js
new file mode 100644
index 0000000..b9fb01f
--- /dev/null
+++ b/crawl/bb-crawl.js
@@ -0,0 +1,165 @@
+'use strict';
+// Metered Browserbase crawl for the anti-bot firms (CBRE, NAI Capital).
+// Strategy: real cloud Chrome runs the site's JS, we INTERCEPT the XHR JSON the
+// search app fires (its own listings API) and parse that — plus a DOM-card fallback.
+// Hard per-firm time cap keeps spend bounded. Cost logged at $0.15/session-minute.
+const fs = require('fs');
+const path = require('path');
+const { chromium } = require('playwright-core');
+const Browserbase = require('@browserbasehq/sdk').default || require('@browserbasehq/sdk');
+const base = require('./base-crawler');
+
+const envTxt = fs.readFileSync(path.join(process.env.HOME, '.claude/skills/browserbase/.env'), 'utf8');
+const API_KEY = (envTxt.match(/BROWSERBASE_API_KEY=(.*)/) || [])[1]?.trim();
+const PROJECT = (envTxt.match(/BROWSERBASE_PROJECT_ID=(.*)/) || [])[1]?.trim();
+const RATE = 0.15; // $/session-minute
+
+const FIRMS = {
+ cbre: { cap_ms: 6 * 60000, entries: [
+ 'https://www.cbre.com/properties/properties-for-lease?sort=Featured&location=Los%20Angeles,%20CA',
+ 'https://www.cbre.com/properties/properties-for-lease' ] },
+ naicapital: { cap_ms: 4 * 60000, entries: [
+ 'https://www.naicapital.com/property-search/',
+ 'https://www.naicapital.com/listings/',
+ 'https://www.naicapital.com/' ] },
+};
+
+const CONSENT_URL = /onetrust|cookielaw|consent|optanon|cookiepedia|geolocation\.onetrust/i;
+const CONSENT_KEYS = /groupname|optanon|cookies|isthirdparty|hostid|firstpartycookie/i;
+const ADDR_KEYS = ['streetaddress', 'address1', 'address', 'street', 'propertyaddress', 'displayaddress'];
+const SIZE_KEYS = ['sqft', 'squarefeet', 'rentablearea', 'size', 'buildingsize', 'availablespace', 'minsize', 'maxsize'];
+const PRICE_KEYS = ['askingrent', 'rent', 'price', 'rate', 'pricepersqft'];
+
+// A candidate is a real listing only if it has an address AND (size OR price OR geo) — and is NOT a consent object.
+function isListing(o) {
+ if (!o || typeof o !== 'object') return false;
+ const keys = Object.keys(o).join(',').toLowerCase();
+ if (CONSENT_KEYS.test(keys)) return false;
+ const hasAddr = ADDR_KEYS.some(k => keys.includes(k));
+ const hasSize = SIZE_KEYS.some(k => keys.includes(k));
+ const hasPrice = PRICE_KEYS.some(k => keys.includes(k));
+ const hasGeo = /latitude|longitude/.test(keys);
+ return hasAddr && (hasSize || hasPrice || hasGeo);
+}
+function harvest(json, out, depth = 0) {
+ if (!json || depth > 7) return;
+ if (Array.isArray(json)) {
+ if (json.length && json.filter(x => isListing(x)).length >= Math.min(2, json.length)) {
+ for (const o of json) if (isListing(o)) out.push(o);
+ } else json.forEach(x => harvest(x, out, depth + 1));
+ } else if (typeof json === 'object') {
+ for (const k of Object.keys(json)) harvest(json[k], out, depth + 1);
+ }
+}
+const pick = (o, keys) => { for (const k of Object.keys(o)) if (keys.some(x => k.toLowerCase() === x || k.toLowerCase().includes(x))) { const v = o[k]; if (v != null && v !== '') return v; } return null; };
+const num = (v) => { if (v == null || typeof v === 'object') return null; const n = String(v).replace(/[^0-9.]/g, ''); return n ? Math.round(+n) : null; };
+// address may be a nested object {street/line1, city, ...} — flatten it to a string
+function addrStr(v) {
+ if (!v) return null;
+ if (typeof v === 'string') return v.slice(0, 200);
+ if (typeof v === 'object') return [v.street || v.line1 || v.streetAddress, v.city || v.locality].filter(Boolean).join(', ').slice(0, 200) || null;
+ return null;
+}
+
+function toListing(o, firm) {
+ if (!isListing(o)) return null;
+ const addr = addrStr(pick(o, ADDR_KEYS));
+ const title = pick(o, ['propertyname', 'name', 'title', 'buildingname']);
+ const url = pick(o, ['detailurl', 'url', 'permalink', 'link', 'propertyurl', 'canonicalurl']);
+ const lat = pick(o, ['latitude', 'lat']), lng = pick(o, ['longitude', 'lng', 'lon']);
+ const id = pick(o, ['listingid', 'propertyid', 'id', 'slug']) || (addr ? addr.slice(0, 40) : null);
+ if (!addr && !id) return null;
+ const cityV = pick(o, ['city', 'cityname', 'locality']);
+ return {
+ external_id: String(id).slice(0, 120),
+ title: String(title || addr || 'Sublease space').slice(0, 200),
+ address: addr,
+ city: typeof cityV === 'string' ? cityV : (typeof pick(o, ADDR_KEYS) === 'object' ? (pick(o, ADDR_KEYS).city || null) : null),
+ state: (() => { const s = pick(o, ['state', 'region']); return typeof s === 'string' ? s : 'CA'; })(),
+ lat: lat ? +lat : null, lng: lng ? +lng : null,
+ space_type: (String(pick(o, ['propertytype', 'type', 'assetclass']) || 'office').toLowerCase().match(/office|retail|industrial|flex|medical|land/) || ['office'])[0],
+ size_sf: num(pick(o, SIZE_KEYS)),
+ price_amount: num(pick(o, PRICE_KEYS)),
+ price_unit: (() => { const u = pick(o, ['rentunit', 'priceunit']); return typeof u === 'string' ? u : 'call for terms'; })(),
+ source_url: url ? (String(url).startsWith('http') ? url : `https://www.${firm}.com${String(url).replace(/^\/?/, '/')}`) : null,
+ raw: o,
+ };
+}
+
+async function crawlFirm(slug) {
+ const cfg = FIRMS[slug];
+ const sponsor = await base.getSponsor(slug);
+ const runId = await base.startRun(sponsor.id);
+ const t0 = Date.now();
+ const captured = [];
+ let session, browser, found = 0, added = 0, note = '';
+ try {
+ const bb = new Browserbase({ apiKey: API_KEY });
+ session = await bb.sessions.create({ projectId: PROJECT });
+ browser = await chromium.connectOverCDP(session.connectUrl);
+ const ctx = browser.contexts()[0]; const page = ctx.pages()[0] || await ctx.newPage();
+ page.on('response', async (res) => {
+ try {
+ const ct = res.headers()['content-type'] || '';
+ if (!/json/.test(ct)) return;
+ const url = res.url();
+ if (/analytics|telemetry|gtm|segment|sentry/i.test(url) || CONSENT_URL.test(url)) return;
+ const body = await res.json().catch(() => null);
+ if (body) { const arr = []; harvest(body, arr); if (arr.length) captured.push({ url, arr }); }
+ } catch { /* ignore */ }
+ });
+ const listingsSoFar = () => captured.reduce((n, c) => n + c.arr.length, 0);
+ // poll: scroll + wait, up to `budget` ms, until we have enough real listings
+ const poll = async (budget) => {
+ const end = Math.min(Date.now() + budget, t0 + cfg.cap_ms);
+ while (Date.now() < end) {
+ await page.mouse.wheel(0, 3000).catch(() => {});
+ await page.waitForTimeout(3500);
+ if (listingsSoFar() >= 5) return true;
+ }
+ return listingsSoFar() > 0;
+ };
+ for (const entry of cfg.entries) {
+ if (Date.now() - t0 > cfg.cap_ms) break;
+ try {
+ await page.goto(entry, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await page.waitForTimeout(3000);
+ // NAI: the real search is behind a JS nav link — click into it from the homepage
+ if (slug === 'naicapital' && !listingsSoFar()) {
+ const link = await page.$('a[href*="propert"], a[href*="listing"], a[href*="for-lease"], a[href*="search"]');
+ if (link) { await link.click().catch(() => {}); await page.waitForTimeout(3500); }
+ }
+ await poll(40000);
+ if (listingsSoFar() >= 5) break;
+ } catch (e) { note += `[${entry.split('/').pop()}:${String(e).slice(0, 40)}] `; }
+ }
+ // Dedupe + convert captured objects → listings
+ const seen = new Set();
+ const objs = captured.sort((a, b) => b.arr.length - a.arr.length).flatMap(c => c.arr);
+ for (const o of objs) {
+ if (Date.now() - t0 > cfg.cap_ms + 30000) break;
+ const L = toListing(o, slug); if (!L) continue;
+ const key = L.external_id + '|' + (L.address || '');
+ if (seen.has(key)) continue; seen.add(key);
+ if (!L.lat && (L.address || L.city)) { const g = await base.geocodeCensus([L.address, L.city, L.state].filter(Boolean).join(', ')); if (g) { L.lat = g.lat; L.lng = g.lng; } }
+ found++; if (await base.upsertListing(sponsor.id, L)) added++;
+ }
+ note = `captured ${captured.length} JSON payloads (${objs.length} candidate rows). ` + note;
+ } catch (e) { note += 'ERR:' + String(e).slice(0, 200); }
+ finally { try { await browser?.close(); } catch {} }
+ const mins = (Date.now() - t0) / 60000;
+ const cost = +(mins * RATE).toFixed(2);
+ await base.finishRun(runId, { status: found ? 'ok' : 'blocked', found, added, cost, notes: note.slice(0, 400) });
+ console.log(`${slug}: ${found} found, ${added} new · ${mins.toFixed(1)} min · $${cost.toFixed(2)} ${note.slice(0, 120)}`);
+ return { cost, found, added };
+}
+
+(async () => {
+ if (!API_KEY || !PROJECT) { console.log('missing Browserbase creds'); process.exit(1); }
+ const targets = process.argv.slice(2).filter(a => FIRMS[a]);
+ const run = targets.length ? targets : Object.keys(FIRMS);
+ let total = 0;
+ for (const slug of run) { const r = await crawlFirm(slug); total += r.cost; }
+ console.log(`\n=== TOTAL BROWSERBASE SPEND: $${total.toFixed(2)} ===`);
+ await base.pool.end();
+})();
diff --git a/package-lock.json b/package-lock.json
index 8e32dac..4cfbe22 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "sublease-agentabrams",
"version": "0.1.0",
"dependencies": {
+ "@browserbasehq/sdk": "^2.16.0",
"express": "^4.19.2",
"pdfkit": "^0.15.0",
"pg": "^8.11.5"
@@ -16,6 +17,21 @@
"playwright-core": "^1.61.1"
}
},
+ "node_modules/@browserbasehq/sdk": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/@browserbasehq/sdk/-/sdk-2.16.0.tgz",
+ "integrity": "sha512-mPAuLRU9jWR7o0KJi9+gQnOBDUSIkoKbbFv4HjrA+80qWVcFacrNPlZmf4mguQnfZ0oP2t5c3ws6yuFyAX9vpA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.3.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz",
@@ -25,6 +41,37 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -38,6 +85,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
@@ -60,6 +119,12 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -202,6 +267,18 @@
"node": ">=0.8"
}
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -319,6 +396,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -423,6 +509,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -438,6 +539,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
@@ -534,6 +644,41 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -702,6 +847,15 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -1085,6 +1239,46 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -1669,6 +1863,12 @@
"node": ">=0.6"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -1688,6 +1888,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
@@ -1741,6 +1947,31 @@
"node": ">= 0.8"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which-boxed-primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
diff --git a/package.json b/package.json
index 800eb77..11d4802 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"snapshot": "node scripts/export-snapshot.js"
},
"dependencies": {
+ "@browserbasehq/sdk": "^2.16.0",
"express": "^4.19.2",
"pdfkit": "^0.15.0",
"pg": "^8.11.5"
@@ -19,4 +20,4 @@
"devDependencies": {
"playwright-core": "^1.61.1"
}
-}
\ No newline at end of file
+}
← a14fd4a Live on Kamatera: snapshot in data/, prod env note
·
back to Sublease Agentabrams
·
auto-save: 2026-07-20T13:32:24 (4 files) — scripts/export-sn 88c13a4 →