@ricsam/r5d-browser 0.0.43 → 0.0.45

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
@@ -7,4 +7,4 @@ npm install -g @ricsam/r5d-browser
7
7
  r5d-browser start
8
8
  ```
9
9
 
10
- The profile is stored under `~/.r5d/browser/profile`. Browser screenshots are session artifacts in r5d.dev and are not retained by the browser process.
10
+ The profile is stored under `~/.r5d/browser/profile`. Completed downloads are retained under `~/.r5d/browser/downloads`; agents can list them and copy a selected file into the active session's artifacts. That artifact is then synchronized to every connected worker. Browser screenshots are session artifacts in r5d.dev and are not retained by the browser process.
package/dist/cjs/main.cjs CHANGED
@@ -25,14 +25,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  var import_node_fs = __toESM(require("node:fs"), 1);
26
26
  var import_node_os = __toESM(require("node:os"), 1);
27
27
  var import_node_path = __toESM(require("node:path"), 1);
28
- var import_node_module = require("node:module");
29
28
  var import_node_child_process = require("node:child_process");
30
29
  var import_promises = require("node:timers/promises");
31
30
  var import_ws = __toESM(require("ws"), 1);
31
+ var import_playwright_install = require("./playwright-install.cjs");
32
32
  const DEFAULT_BASE_URL = "https://r5d.dev";
33
33
  const PACKAGE_NAME = "@ricsam/r5d-browser";
34
34
  const RECONNECT_DELAY_MS = 2e3;
35
35
  const SERVER_HEARTBEAT_TIMEOUT_MS = 45e3;
36
+ const MAX_DOWNLOAD_CHUNK_BYTES = 1024 * 1024;
36
37
  function browserRoot() {
37
38
  return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "browser");
38
39
  }
@@ -146,8 +147,7 @@ function resolveConnection(options) {
146
147
  function installChromium(browsersPath) {
147
148
  import_node_fs.default.mkdirSync(browsersPath, { recursive: true });
148
149
  process.env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
149
- const require2 = (0, import_node_module.createRequire)(process.argv[1] ?? import_node_path.default.join(process.cwd(), "package.json"));
150
- const cliPath = require2.resolve("playwright/cli");
150
+ const cliPath = (0, import_playwright_install.resolvePlaywrightCliPath)();
151
151
  process.stdout.write(`[r5d-browser] Installing Chromium under ${browsersPath}...
152
152
  `);
153
153
  const result = (0, import_node_child_process.spawnSync)(process.execPath, [cliPath, "install", "chromium"], {
@@ -170,27 +170,133 @@ function loadInstanceId() {
170
170
  return id;
171
171
  }
172
172
  class BrowserRuntime {
173
- constructor(context) {
173
+ constructor(context, downloadsPath) {
174
174
  this.context = context;
175
+ this.downloadsPath = downloadsPath;
176
+ import_node_fs.default.mkdirSync(downloadsPath, { recursive: true });
175
177
  for (const page of context.pages()) this.track(page);
176
178
  context.on("page", (page) => this.track(page));
177
179
  }
178
180
  context;
181
+ downloadsPath;
179
182
  ids = /* @__PURE__ */ new WeakMap();
180
183
  pages = /* @__PURE__ */ new Map();
181
184
  cursor = /* @__PURE__ */ new Map();
185
+ downloadSaveQueue = Promise.resolve();
182
186
  track(page) {
183
187
  const existing = this.ids.get(page);
184
188
  if (existing) return existing;
185
189
  const id = crypto.randomUUID();
186
190
  this.ids.set(page, id);
187
191
  this.pages.set(id, page);
192
+ page.on("download", (download) => {
193
+ this.downloadSaveQueue = this.downloadSaveQueue.then(() => this.persistDownload(download)).catch((error) => {
194
+ process.stderr.write(`[r5d-browser] failed to save download: ${error instanceof Error ? error.message : String(error)}
195
+ `);
196
+ });
197
+ });
188
198
  page.once("close", () => {
189
199
  this.pages.delete(id);
190
200
  this.cursor.delete(id);
191
201
  });
192
202
  return id;
193
203
  }
204
+ safeDownloadFilename(suggestedFilename) {
205
+ const basename = import_node_path.default.basename(suggestedFilename).replace(/[\\/\0]/g, "-").trim();
206
+ if (!basename || basename === "." || basename === "..") return "download";
207
+ const extension = import_node_path.default.extname(basename).slice(0, 30);
208
+ let stem = basename.slice(0, basename.length - import_node_path.default.extname(basename).length) || "download";
209
+ while (Buffer.byteLength(`${stem}${extension}`) > 200) stem = stem.slice(0, -1);
210
+ return `${stem || "download"}${extension}`;
211
+ }
212
+ availableDownloadPath(suggestedFilename) {
213
+ const filename = this.safeDownloadFilename(suggestedFilename);
214
+ const extension = import_node_path.default.extname(filename);
215
+ const stem = filename.slice(0, filename.length - extension.length) || "download";
216
+ for (let suffix = 1; ; suffix += 1) {
217
+ const candidate = suffix === 1 ? filename : `${stem}-${suffix}${extension}`;
218
+ const candidatePath = import_node_path.default.join(this.downloadsPath, candidate);
219
+ if (!import_node_fs.default.existsSync(candidatePath)) return candidatePath;
220
+ }
221
+ }
222
+ async persistDownload(download) {
223
+ const failure = await download.failure();
224
+ if (failure) throw new Error(failure);
225
+ const targetPath = this.availableDownloadPath(download.suggestedFilename());
226
+ await download.saveAs(targetPath);
227
+ process.stdout.write(`[r5d-browser] downloaded ${import_node_path.default.basename(targetPath)}
228
+ `);
229
+ }
230
+ downloadId(filename) {
231
+ return Buffer.from(filename, "utf8").toString("base64url");
232
+ }
233
+ resolveDownload(downloadId) {
234
+ if (typeof downloadId !== "string" || !downloadId) throw new Error("downloadId is required.");
235
+ const filename = Buffer.from(downloadId, "base64url").toString("utf8");
236
+ if (this.downloadId(filename) !== downloadId || filename !== import_node_path.default.basename(filename) || filename.includes("\0")) {
237
+ throw new Error("Invalid downloadId.");
238
+ }
239
+ const filePath = import_node_path.default.join(this.downloadsPath, filename);
240
+ let stats;
241
+ try {
242
+ stats = import_node_fs.default.lstatSync(filePath);
243
+ } catch {
244
+ throw new Error(`Browser download ${downloadId} no longer exists.`);
245
+ }
246
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Browser download is not a regular file.");
247
+ return { filename, filePath, stats };
248
+ }
249
+ async listDownloads() {
250
+ await this.downloadSaveQueue;
251
+ return import_node_fs.default.readdirSync(this.downloadsPath, { withFileTypes: true }).flatMap((entry) => {
252
+ if (!entry.isFile() || entry.name.startsWith(".")) return [];
253
+ const { filename, stats } = this.resolveDownload(this.downloadId(entry.name));
254
+ return [
255
+ {
256
+ downloadId: this.downloadId(filename),
257
+ filename,
258
+ size: stats.size,
259
+ modifiedAt: stats.mtime.toISOString()
260
+ }
261
+ ];
262
+ }).sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt) || left.filename.localeCompare(right.filename));
263
+ }
264
+ async getDownloadChunk(input) {
265
+ await this.downloadSaveQueue;
266
+ const { filename, filePath, stats } = this.resolveDownload(input.downloadId);
267
+ const offset = input.offset === void 0 ? 0 : Number(input.offset);
268
+ const requestedBytes = input.maxBytes === void 0 ? MAX_DOWNLOAD_CHUNK_BYTES : Number(input.maxBytes);
269
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > stats.size) throw new Error("Invalid download offset.");
270
+ if (!Number.isSafeInteger(requestedBytes) || requestedBytes < 1) throw new Error("Invalid download chunk size.");
271
+ const chunkSize = Math.min(requestedBytes, MAX_DOWNLOAD_CHUNK_BYTES, stats.size - offset);
272
+ const bytes = Buffer.alloc(chunkSize);
273
+ const handle = import_node_fs.default.openSync(filePath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
274
+ let bytesRead = 0;
275
+ try {
276
+ const openedStats = import_node_fs.default.fstatSync(handle);
277
+ if (!openedStats.isFile() || openedStats.dev !== stats.dev || openedStats.ino !== stats.ino) {
278
+ throw new Error("Browser download changed before it could be read.");
279
+ }
280
+ bytesRead = import_node_fs.default.readSync(handle, bytes, 0, chunkSize, offset);
281
+ const completedStats = import_node_fs.default.fstatSync(handle);
282
+ if (completedStats.size !== stats.size || completedStats.mtimeMs !== stats.mtimeMs) {
283
+ throw new Error("Browser download changed while it was being read.");
284
+ }
285
+ } finally {
286
+ import_node_fs.default.closeSync(handle);
287
+ }
288
+ const nextOffset = offset + bytesRead;
289
+ return {
290
+ downloadId: this.downloadId(filename),
291
+ filename,
292
+ size: stats.size,
293
+ modifiedAt: stats.mtime.toISOString(),
294
+ offset,
295
+ nextOffset,
296
+ eof: nextOffset === stats.size,
297
+ base64: bytes.subarray(0, bytesRead).toString("base64")
298
+ };
299
+ }
194
300
  page(tabId) {
195
301
  if (typeof tabId !== "string") throw new Error("tabId is required.");
196
302
  const page = this.pages.get(tabId);
@@ -214,6 +320,8 @@ class BrowserRuntime {
214
320
  }
215
321
  async execute(action, input) {
216
322
  if (action === "list_tabs") return { tabs: await this.listTabs() };
323
+ if (action === "list_downloads") return { downloads: await this.listDownloads() };
324
+ if (action === "get_download") return await this.getDownloadChunk(input);
217
325
  if (action === "open_tab") {
218
326
  const previous = this.context.pages().findLast((candidate) => !candidate.isClosed());
219
327
  let page2;
@@ -395,6 +503,8 @@ async function start(options) {
395
503
  const { baseUrl, token } = resolveConnection(options);
396
504
  const browsersPath = import_node_path.default.resolve(options.browsersPath ?? import_node_path.default.join(browserRoot(), "browsers"));
397
505
  const profilePath = import_node_path.default.resolve(options.profilePath ?? import_node_path.default.join(browserRoot(), "profile"));
506
+ const downloadsPath = import_node_path.default.join(browserRoot(), "downloads");
507
+ const playwrightDownloadsPath = import_node_path.default.join(downloadsPath, ".playwright");
398
508
  process.env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
399
509
  let playwright = await import("playwright");
400
510
  if (!import_node_fs.default.existsSync(playwright.chromium.executablePath())) {
@@ -402,15 +512,18 @@ async function start(options) {
402
512
  playwright = await import("playwright");
403
513
  }
404
514
  import_node_fs.default.mkdirSync(profilePath, { recursive: true });
515
+ import_node_fs.default.mkdirSync(playwrightDownloadsPath, { recursive: true });
405
516
  const context = await playwright.chromium.launchPersistentContext(profilePath, {
406
517
  headless: false,
407
518
  viewport: { width: 1440, height: 900 },
408
519
  acceptDownloads: true,
409
- downloadsPath: import_node_path.default.join(browserRoot(), "downloads")
520
+ downloadsPath: playwrightDownloadsPath
410
521
  });
411
522
  if (context.pages().length === 0) await context.newPage();
412
- const runtime = new BrowserRuntime(context);
523
+ const runtime = new BrowserRuntime(context, downloadsPath);
413
524
  process.stdout.write(`[r5d-browser] profile: ${profilePath}
525
+ `);
526
+ process.stdout.write(`[r5d-browser] downloads: ${downloadsPath}
414
527
  `);
415
528
  process.stdout.write(`[r5d-browser] agents use synthetic Chromium input; your system pointer and keyboard stay untouched.
416
529
  `);
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var playwright_install_exports = {};
30
+ __export(playwright_install_exports, {
31
+ resolvePlaywrightCliPath: () => resolvePlaywrightCliPath
32
+ });
33
+ module.exports = __toCommonJS(playwright_install_exports);
34
+ var import_node_fs = __toESM(require("node:fs"), 1);
35
+ var import_node_path = __toESM(require("node:path"), 1);
36
+ var import_node_module = require("node:module");
37
+ function resolvePlaywrightCliPath(fromPath = process.argv[1] ?? import_node_path.default.join(process.cwd(), "package.json")) {
38
+ const require2 = (0, import_node_module.createRequire)(fromPath);
39
+ const packageJsonPath = require2.resolve("playwright/package.json");
40
+ const cliPath = import_node_path.default.join(import_node_path.default.dirname(packageJsonPath), "cli.js");
41
+ if (!import_node_fs.default.existsSync(cliPath)) {
42
+ throw new Error(`The Playwright CLI was not found next to ${packageJsonPath}. Reinstall @ricsam/r5d-browser.`);
43
+ }
44
+ return cliPath;
45
+ }
46
+ // Annotate the CommonJS export names for ESM import in node:
47
+ 0 && (module.exports = {
48
+ resolvePlaywrightCliPath
49
+ });
package/dist/mjs/main.mjs CHANGED
@@ -2,14 +2,15 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { createRequire } from "node:module";
6
5
  import { spawnSync } from "node:child_process";
7
6
  import { setTimeout as sleep } from "node:timers/promises";
8
7
  import WebSocket from "ws";
8
+ import { resolvePlaywrightCliPath } from "./playwright-install.mjs";
9
9
  const DEFAULT_BASE_URL = "https://r5d.dev";
10
10
  const PACKAGE_NAME = "@ricsam/r5d-browser";
11
11
  const RECONNECT_DELAY_MS = 2e3;
12
12
  const SERVER_HEARTBEAT_TIMEOUT_MS = 45e3;
13
+ const MAX_DOWNLOAD_CHUNK_BYTES = 1024 * 1024;
13
14
  function browserRoot() {
14
15
  return path.join(os.homedir(), ".r5d", "browser");
15
16
  }
@@ -123,8 +124,7 @@ function resolveConnection(options) {
123
124
  function installChromium(browsersPath) {
124
125
  fs.mkdirSync(browsersPath, { recursive: true });
125
126
  process.env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
126
- const require2 = createRequire(process.argv[1] ?? path.join(process.cwd(), "package.json"));
127
- const cliPath = require2.resolve("playwright/cli");
127
+ const cliPath = resolvePlaywrightCliPath();
128
128
  process.stdout.write(`[r5d-browser] Installing Chromium under ${browsersPath}...
129
129
  `);
130
130
  const result = spawnSync(process.execPath, [cliPath, "install", "chromium"], {
@@ -147,27 +147,133 @@ function loadInstanceId() {
147
147
  return id;
148
148
  }
149
149
  class BrowserRuntime {
150
- constructor(context) {
150
+ constructor(context, downloadsPath) {
151
151
  this.context = context;
152
+ this.downloadsPath = downloadsPath;
153
+ fs.mkdirSync(downloadsPath, { recursive: true });
152
154
  for (const page of context.pages()) this.track(page);
153
155
  context.on("page", (page) => this.track(page));
154
156
  }
155
157
  context;
158
+ downloadsPath;
156
159
  ids = /* @__PURE__ */ new WeakMap();
157
160
  pages = /* @__PURE__ */ new Map();
158
161
  cursor = /* @__PURE__ */ new Map();
162
+ downloadSaveQueue = Promise.resolve();
159
163
  track(page) {
160
164
  const existing = this.ids.get(page);
161
165
  if (existing) return existing;
162
166
  const id = crypto.randomUUID();
163
167
  this.ids.set(page, id);
164
168
  this.pages.set(id, page);
169
+ page.on("download", (download) => {
170
+ this.downloadSaveQueue = this.downloadSaveQueue.then(() => this.persistDownload(download)).catch((error) => {
171
+ process.stderr.write(`[r5d-browser] failed to save download: ${error instanceof Error ? error.message : String(error)}
172
+ `);
173
+ });
174
+ });
165
175
  page.once("close", () => {
166
176
  this.pages.delete(id);
167
177
  this.cursor.delete(id);
168
178
  });
169
179
  return id;
170
180
  }
181
+ safeDownloadFilename(suggestedFilename) {
182
+ const basename = path.basename(suggestedFilename).replace(/[\\/\0]/g, "-").trim();
183
+ if (!basename || basename === "." || basename === "..") return "download";
184
+ const extension = path.extname(basename).slice(0, 30);
185
+ let stem = basename.slice(0, basename.length - path.extname(basename).length) || "download";
186
+ while (Buffer.byteLength(`${stem}${extension}`) > 200) stem = stem.slice(0, -1);
187
+ return `${stem || "download"}${extension}`;
188
+ }
189
+ availableDownloadPath(suggestedFilename) {
190
+ const filename = this.safeDownloadFilename(suggestedFilename);
191
+ const extension = path.extname(filename);
192
+ const stem = filename.slice(0, filename.length - extension.length) || "download";
193
+ for (let suffix = 1; ; suffix += 1) {
194
+ const candidate = suffix === 1 ? filename : `${stem}-${suffix}${extension}`;
195
+ const candidatePath = path.join(this.downloadsPath, candidate);
196
+ if (!fs.existsSync(candidatePath)) return candidatePath;
197
+ }
198
+ }
199
+ async persistDownload(download) {
200
+ const failure = await download.failure();
201
+ if (failure) throw new Error(failure);
202
+ const targetPath = this.availableDownloadPath(download.suggestedFilename());
203
+ await download.saveAs(targetPath);
204
+ process.stdout.write(`[r5d-browser] downloaded ${path.basename(targetPath)}
205
+ `);
206
+ }
207
+ downloadId(filename) {
208
+ return Buffer.from(filename, "utf8").toString("base64url");
209
+ }
210
+ resolveDownload(downloadId) {
211
+ if (typeof downloadId !== "string" || !downloadId) throw new Error("downloadId is required.");
212
+ const filename = Buffer.from(downloadId, "base64url").toString("utf8");
213
+ if (this.downloadId(filename) !== downloadId || filename !== path.basename(filename) || filename.includes("\0")) {
214
+ throw new Error("Invalid downloadId.");
215
+ }
216
+ const filePath = path.join(this.downloadsPath, filename);
217
+ let stats;
218
+ try {
219
+ stats = fs.lstatSync(filePath);
220
+ } catch {
221
+ throw new Error(`Browser download ${downloadId} no longer exists.`);
222
+ }
223
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Browser download is not a regular file.");
224
+ return { filename, filePath, stats };
225
+ }
226
+ async listDownloads() {
227
+ await this.downloadSaveQueue;
228
+ return fs.readdirSync(this.downloadsPath, { withFileTypes: true }).flatMap((entry) => {
229
+ if (!entry.isFile() || entry.name.startsWith(".")) return [];
230
+ const { filename, stats } = this.resolveDownload(this.downloadId(entry.name));
231
+ return [
232
+ {
233
+ downloadId: this.downloadId(filename),
234
+ filename,
235
+ size: stats.size,
236
+ modifiedAt: stats.mtime.toISOString()
237
+ }
238
+ ];
239
+ }).sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt) || left.filename.localeCompare(right.filename));
240
+ }
241
+ async getDownloadChunk(input) {
242
+ await this.downloadSaveQueue;
243
+ const { filename, filePath, stats } = this.resolveDownload(input.downloadId);
244
+ const offset = input.offset === void 0 ? 0 : Number(input.offset);
245
+ const requestedBytes = input.maxBytes === void 0 ? MAX_DOWNLOAD_CHUNK_BYTES : Number(input.maxBytes);
246
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > stats.size) throw new Error("Invalid download offset.");
247
+ if (!Number.isSafeInteger(requestedBytes) || requestedBytes < 1) throw new Error("Invalid download chunk size.");
248
+ const chunkSize = Math.min(requestedBytes, MAX_DOWNLOAD_CHUNK_BYTES, stats.size - offset);
249
+ const bytes = Buffer.alloc(chunkSize);
250
+ const handle = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
251
+ let bytesRead = 0;
252
+ try {
253
+ const openedStats = fs.fstatSync(handle);
254
+ if (!openedStats.isFile() || openedStats.dev !== stats.dev || openedStats.ino !== stats.ino) {
255
+ throw new Error("Browser download changed before it could be read.");
256
+ }
257
+ bytesRead = fs.readSync(handle, bytes, 0, chunkSize, offset);
258
+ const completedStats = fs.fstatSync(handle);
259
+ if (completedStats.size !== stats.size || completedStats.mtimeMs !== stats.mtimeMs) {
260
+ throw new Error("Browser download changed while it was being read.");
261
+ }
262
+ } finally {
263
+ fs.closeSync(handle);
264
+ }
265
+ const nextOffset = offset + bytesRead;
266
+ return {
267
+ downloadId: this.downloadId(filename),
268
+ filename,
269
+ size: stats.size,
270
+ modifiedAt: stats.mtime.toISOString(),
271
+ offset,
272
+ nextOffset,
273
+ eof: nextOffset === stats.size,
274
+ base64: bytes.subarray(0, bytesRead).toString("base64")
275
+ };
276
+ }
171
277
  page(tabId) {
172
278
  if (typeof tabId !== "string") throw new Error("tabId is required.");
173
279
  const page = this.pages.get(tabId);
@@ -191,6 +297,8 @@ class BrowserRuntime {
191
297
  }
192
298
  async execute(action, input) {
193
299
  if (action === "list_tabs") return { tabs: await this.listTabs() };
300
+ if (action === "list_downloads") return { downloads: await this.listDownloads() };
301
+ if (action === "get_download") return await this.getDownloadChunk(input);
194
302
  if (action === "open_tab") {
195
303
  const previous = this.context.pages().findLast((candidate) => !candidate.isClosed());
196
304
  let page2;
@@ -372,6 +480,8 @@ async function start(options) {
372
480
  const { baseUrl, token } = resolveConnection(options);
373
481
  const browsersPath = path.resolve(options.browsersPath ?? path.join(browserRoot(), "browsers"));
374
482
  const profilePath = path.resolve(options.profilePath ?? path.join(browserRoot(), "profile"));
483
+ const downloadsPath = path.join(browserRoot(), "downloads");
484
+ const playwrightDownloadsPath = path.join(downloadsPath, ".playwright");
375
485
  process.env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
376
486
  let playwright = await import("playwright");
377
487
  if (!fs.existsSync(playwright.chromium.executablePath())) {
@@ -379,15 +489,18 @@ async function start(options) {
379
489
  playwright = await import("playwright");
380
490
  }
381
491
  fs.mkdirSync(profilePath, { recursive: true });
492
+ fs.mkdirSync(playwrightDownloadsPath, { recursive: true });
382
493
  const context = await playwright.chromium.launchPersistentContext(profilePath, {
383
494
  headless: false,
384
495
  viewport: { width: 1440, height: 900 },
385
496
  acceptDownloads: true,
386
- downloadsPath: path.join(browserRoot(), "downloads")
497
+ downloadsPath: playwrightDownloadsPath
387
498
  });
388
499
  if (context.pages().length === 0) await context.newPage();
389
- const runtime = new BrowserRuntime(context);
500
+ const runtime = new BrowserRuntime(context, downloadsPath);
390
501
  process.stdout.write(`[r5d-browser] profile: ${profilePath}
502
+ `);
503
+ process.stdout.write(`[r5d-browser] downloads: ${downloadsPath}
391
504
  `);
392
505
  process.stdout.write(`[r5d-browser] agents use synthetic Chromium input; your system pointer and keyboard stay untouched.
393
506
  `);
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,15 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ function resolvePlaywrightCliPath(fromPath = process.argv[1] ?? path.join(process.cwd(), "package.json")) {
5
+ const require2 = createRequire(fromPath);
6
+ const packageJsonPath = require2.resolve("playwright/package.json");
7
+ const cliPath = path.join(path.dirname(packageJsonPath), "cli.js");
8
+ if (!fs.existsSync(cliPath)) {
9
+ throw new Error(`The Playwright CLI was not found next to ${packageJsonPath}. Reinstall @ricsam/r5d-browser.`);
10
+ }
11
+ return cliPath;
12
+ }
13
+ export {
14
+ resolvePlaywrightCliPath
15
+ };
@@ -0,0 +1 @@
1
+ export declare function resolvePlaywrightCliPath(fromPath?: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",