@ricsam/r5d-browser 0.0.44 → 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 +1 -1
- package/dist/cjs/main.cjs +117 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +117 -3
- package/dist/mjs/package.json +1 -1
- package/package.json +1 -1
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
|
@@ -33,6 +33,7 @@ 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
|
}
|
|
@@ -169,27 +170,133 @@ function loadInstanceId() {
|
|
|
169
170
|
return id;
|
|
170
171
|
}
|
|
171
172
|
class BrowserRuntime {
|
|
172
|
-
constructor(context) {
|
|
173
|
+
constructor(context, downloadsPath) {
|
|
173
174
|
this.context = context;
|
|
175
|
+
this.downloadsPath = downloadsPath;
|
|
176
|
+
import_node_fs.default.mkdirSync(downloadsPath, { recursive: true });
|
|
174
177
|
for (const page of context.pages()) this.track(page);
|
|
175
178
|
context.on("page", (page) => this.track(page));
|
|
176
179
|
}
|
|
177
180
|
context;
|
|
181
|
+
downloadsPath;
|
|
178
182
|
ids = /* @__PURE__ */ new WeakMap();
|
|
179
183
|
pages = /* @__PURE__ */ new Map();
|
|
180
184
|
cursor = /* @__PURE__ */ new Map();
|
|
185
|
+
downloadSaveQueue = Promise.resolve();
|
|
181
186
|
track(page) {
|
|
182
187
|
const existing = this.ids.get(page);
|
|
183
188
|
if (existing) return existing;
|
|
184
189
|
const id = crypto.randomUUID();
|
|
185
190
|
this.ids.set(page, id);
|
|
186
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
|
+
});
|
|
187
198
|
page.once("close", () => {
|
|
188
199
|
this.pages.delete(id);
|
|
189
200
|
this.cursor.delete(id);
|
|
190
201
|
});
|
|
191
202
|
return id;
|
|
192
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
|
+
}
|
|
193
300
|
page(tabId) {
|
|
194
301
|
if (typeof tabId !== "string") throw new Error("tabId is required.");
|
|
195
302
|
const page = this.pages.get(tabId);
|
|
@@ -213,6 +320,8 @@ class BrowserRuntime {
|
|
|
213
320
|
}
|
|
214
321
|
async execute(action, input) {
|
|
215
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);
|
|
216
325
|
if (action === "open_tab") {
|
|
217
326
|
const previous = this.context.pages().findLast((candidate) => !candidate.isClosed());
|
|
218
327
|
let page2;
|
|
@@ -394,6 +503,8 @@ async function start(options) {
|
|
|
394
503
|
const { baseUrl, token } = resolveConnection(options);
|
|
395
504
|
const browsersPath = import_node_path.default.resolve(options.browsersPath ?? import_node_path.default.join(browserRoot(), "browsers"));
|
|
396
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");
|
|
397
508
|
process.env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
|
|
398
509
|
let playwright = await import("playwright");
|
|
399
510
|
if (!import_node_fs.default.existsSync(playwright.chromium.executablePath())) {
|
|
@@ -401,15 +512,18 @@ async function start(options) {
|
|
|
401
512
|
playwright = await import("playwright");
|
|
402
513
|
}
|
|
403
514
|
import_node_fs.default.mkdirSync(profilePath, { recursive: true });
|
|
515
|
+
import_node_fs.default.mkdirSync(playwrightDownloadsPath, { recursive: true });
|
|
404
516
|
const context = await playwright.chromium.launchPersistentContext(profilePath, {
|
|
405
517
|
headless: false,
|
|
406
518
|
viewport: { width: 1440, height: 900 },
|
|
407
519
|
acceptDownloads: true,
|
|
408
|
-
downloadsPath:
|
|
520
|
+
downloadsPath: playwrightDownloadsPath
|
|
409
521
|
});
|
|
410
522
|
if (context.pages().length === 0) await context.newPage();
|
|
411
|
-
const runtime = new BrowserRuntime(context);
|
|
523
|
+
const runtime = new BrowserRuntime(context, downloadsPath);
|
|
412
524
|
process.stdout.write(`[r5d-browser] profile: ${profilePath}
|
|
525
|
+
`);
|
|
526
|
+
process.stdout.write(`[r5d-browser] downloads: ${downloadsPath}
|
|
413
527
|
`);
|
|
414
528
|
process.stdout.write(`[r5d-browser] agents use synthetic Chromium input; your system pointer and keyboard stay untouched.
|
|
415
529
|
`);
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/main.mjs
CHANGED
|
@@ -10,6 +10,7 @@ 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
|
}
|
|
@@ -146,27 +147,133 @@ function loadInstanceId() {
|
|
|
146
147
|
return id;
|
|
147
148
|
}
|
|
148
149
|
class BrowserRuntime {
|
|
149
|
-
constructor(context) {
|
|
150
|
+
constructor(context, downloadsPath) {
|
|
150
151
|
this.context = context;
|
|
152
|
+
this.downloadsPath = downloadsPath;
|
|
153
|
+
fs.mkdirSync(downloadsPath, { recursive: true });
|
|
151
154
|
for (const page of context.pages()) this.track(page);
|
|
152
155
|
context.on("page", (page) => this.track(page));
|
|
153
156
|
}
|
|
154
157
|
context;
|
|
158
|
+
downloadsPath;
|
|
155
159
|
ids = /* @__PURE__ */ new WeakMap();
|
|
156
160
|
pages = /* @__PURE__ */ new Map();
|
|
157
161
|
cursor = /* @__PURE__ */ new Map();
|
|
162
|
+
downloadSaveQueue = Promise.resolve();
|
|
158
163
|
track(page) {
|
|
159
164
|
const existing = this.ids.get(page);
|
|
160
165
|
if (existing) return existing;
|
|
161
166
|
const id = crypto.randomUUID();
|
|
162
167
|
this.ids.set(page, id);
|
|
163
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
|
+
});
|
|
164
175
|
page.once("close", () => {
|
|
165
176
|
this.pages.delete(id);
|
|
166
177
|
this.cursor.delete(id);
|
|
167
178
|
});
|
|
168
179
|
return id;
|
|
169
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
|
+
}
|
|
170
277
|
page(tabId) {
|
|
171
278
|
if (typeof tabId !== "string") throw new Error("tabId is required.");
|
|
172
279
|
const page = this.pages.get(tabId);
|
|
@@ -190,6 +297,8 @@ class BrowserRuntime {
|
|
|
190
297
|
}
|
|
191
298
|
async execute(action, input) {
|
|
192
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);
|
|
193
302
|
if (action === "open_tab") {
|
|
194
303
|
const previous = this.context.pages().findLast((candidate) => !candidate.isClosed());
|
|
195
304
|
let page2;
|
|
@@ -371,6 +480,8 @@ async function start(options) {
|
|
|
371
480
|
const { baseUrl, token } = resolveConnection(options);
|
|
372
481
|
const browsersPath = path.resolve(options.browsersPath ?? path.join(browserRoot(), "browsers"));
|
|
373
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");
|
|
374
485
|
process.env.PLAYWRIGHT_BROWSERS_PATH = browsersPath;
|
|
375
486
|
let playwright = await import("playwright");
|
|
376
487
|
if (!fs.existsSync(playwright.chromium.executablePath())) {
|
|
@@ -378,15 +489,18 @@ async function start(options) {
|
|
|
378
489
|
playwright = await import("playwright");
|
|
379
490
|
}
|
|
380
491
|
fs.mkdirSync(profilePath, { recursive: true });
|
|
492
|
+
fs.mkdirSync(playwrightDownloadsPath, { recursive: true });
|
|
381
493
|
const context = await playwright.chromium.launchPersistentContext(profilePath, {
|
|
382
494
|
headless: false,
|
|
383
495
|
viewport: { width: 1440, height: 900 },
|
|
384
496
|
acceptDownloads: true,
|
|
385
|
-
downloadsPath:
|
|
497
|
+
downloadsPath: playwrightDownloadsPath
|
|
386
498
|
});
|
|
387
499
|
if (context.pages().length === 0) await context.newPage();
|
|
388
|
-
const runtime = new BrowserRuntime(context);
|
|
500
|
+
const runtime = new BrowserRuntime(context, downloadsPath);
|
|
389
501
|
process.stdout.write(`[r5d-browser] profile: ${profilePath}
|
|
502
|
+
`);
|
|
503
|
+
process.stdout.write(`[r5d-browser] downloads: ${downloadsPath}
|
|
390
504
|
`);
|
|
391
505
|
process.stdout.write(`[r5d-browser] agents use synthetic Chromium input; your system pointer and keyboard stay untouched.
|
|
392
506
|
`);
|
package/dist/mjs/package.json
CHANGED