@ricsam/r5d-browser 0.0.66 → 0.0.67

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
@@ -22,6 +22,8 @@ r5d-browser start --chrome-path "/Applications/Google Chrome.app/Contents/MacOS/
22
22
 
23
23
  The profile is stored under `~/.r5d/browser/chrome-profile`. The previous Chrome for Testing profile at `~/.r5d/browser/profile` is preserved but is not migrated automatically, so sign in again the first time you use the new profile. Browser binaries previously installed under `~/.r5d/browser/browsers` are also left untouched, but new versions of `r5d-browser` do not use them. 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.
24
24
 
25
+ Agents running JavaScript in a page can save captured text straight into the active session's artifacts by calling `r5dCreateArtifact(filename, content)`, which takes two strings and must be called before the agent's code returns. Each captured file is written to the session artifacts in r5d.dev, synchronized to every connected worker, and reported back as a `$R5D_ARTIFACTS_DIR` path; a single call may create at most 32 files totalling 4 MiB. The function is passed into the agent's code rather than assigned to `window`, so pages can neither call it nor use it to detect automation.
26
+
25
27
  Agents can also create loopback-only TCP port forwards from this Mac to the worker bound to a chat. For example, a forward can map Chrome's `localhost:4323` to `127.0.0.1:3232` on that worker. The relay preserves raw TCP traffic, including HTTP, HTTPS, WebSockets, HMR, and SSE; it never exposes a public listener or permits an arbitrary target host.
26
28
 
27
29
  Port forwards remain active across agent turns and r5d.dev control-connection reconnects. They are owned by the running `r5d-browser` process, so restarting that process ends them. Agents can list and stop forwards, and the user can inspect or stop them from Settings.
@@ -34,6 +34,9 @@ module.exports = __toCommonJS(browser_runtime_exports);
34
34
  var import_node_fs = __toESM(require("node:fs"), 1);
35
35
  var import_node_path = __toESM(require("node:path"), 1);
36
36
  const MAX_DOWNLOAD_CHUNK_BYTES = 1024 * 1024;
37
+ const MAX_RUN_JS_RESULT_BYTES = 1024 * 1024;
38
+ const MAX_RUN_JS_ARTIFACT_BYTES = 4 * 1024 * 1024;
39
+ const MAX_RUN_JS_ARTIFACT_COUNT = 32;
37
40
  class BrowserRuntime {
38
41
  constructor(context, downloadsPath) {
39
42
  this.context = context;
@@ -272,17 +275,46 @@ class BrowserRuntime {
272
275
  return { tabId, characters: input.text.length };
273
276
  case "run_js": {
274
277
  if (typeof input.code !== "string") throw new Error("run_js requires code.");
275
- const result = await page.evaluate(async (code) => {
276
- const invoke = new Function(`"use strict"; return (async () => {
278
+ const evaluated = await page.evaluate(
279
+ async ({ code, maxBytes, maxCount }) => {
280
+ const artifacts = [];
281
+ let totalBytes = 0;
282
+ const r5dCreateArtifact = (filename, content) => {
283
+ if (typeof filename !== "string" || !filename.trim()) {
284
+ throw new TypeError("r5dCreateArtifact(filename, content): filename must be a non-empty string.");
285
+ }
286
+ if (typeof content !== "string") {
287
+ throw new TypeError(
288
+ `r5dCreateArtifact(${JSON.stringify(filename)}, content): content must be a string. Use JSON.stringify(value) for objects.`
289
+ );
290
+ }
291
+ if (artifacts.length >= maxCount) {
292
+ throw new RangeError(`r5dCreateArtifact: at most ${maxCount} artifacts can be created in one browser_run_js call.`);
293
+ }
294
+ totalBytes += new TextEncoder().encode(content).length;
295
+ if (totalBytes > maxBytes) {
296
+ throw new RangeError(`r5dCreateArtifact: artifact content exceeded the ${maxBytes} byte budget for one browser_run_js call.`);
297
+ }
298
+ artifacts.push({ filename, content });
299
+ return filename;
300
+ };
301
+ const invoke = new Function("r5dCreateArtifact", `"use strict"; return (async () => {
277
302
  ${code}
278
303
  })()`);
279
- return await invoke();
280
- }, input.code);
281
- const serialized = JSON.stringify(result);
282
- if (serialized && Buffer.byteLength(serialized) > 1024 * 1024) {
304
+ try {
305
+ return { result: await invoke(r5dCreateArtifact), artifacts };
306
+ } catch (error) {
307
+ if (artifacts.length === 0) throw error;
308
+ return { artifacts, failure: error instanceof Error ? error.message : String(error) };
309
+ }
310
+ },
311
+ { code: input.code, maxBytes: MAX_RUN_JS_ARTIFACT_BYTES, maxCount: MAX_RUN_JS_ARTIFACT_COUNT }
312
+ );
313
+ const serialized = JSON.stringify(evaluated.result);
314
+ if (serialized && Buffer.byteLength(serialized) > MAX_RUN_JS_RESULT_BYTES) {
283
315
  throw new Error("Browser JavaScript result exceeded the 1 MiB limit.");
284
316
  }
285
- return { tabId, result };
317
+ return { tabId, result: evaluated.result, artifacts: evaluated.artifacts, failure: evaluated.failure };
286
318
  }
287
319
  default:
288
320
  throw new Error(`Unsupported browser operation: ${action}`);
package/dist/cjs/main.cjs CHANGED
@@ -150,7 +150,7 @@ async function connectLoop(params) {
150
150
  version: findVersion(),
151
151
  chromiumVersion: params.chromiumVersion,
152
152
  profilePath: params.profilePath,
153
- capabilities: { portForwarding: true }
153
+ capabilities: { portForwarding: true, runJsArtifacts: true }
154
154
  },
155
155
  tabs: await params.runtime.listTabs(),
156
156
  portForwards: params.portForwards.list()
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "type": "commonjs"
5
5
  }
@@ -1,6 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  const MAX_DOWNLOAD_CHUNK_BYTES = 1024 * 1024;
4
+ const MAX_RUN_JS_RESULT_BYTES = 1024 * 1024;
5
+ const MAX_RUN_JS_ARTIFACT_BYTES = 4 * 1024 * 1024;
6
+ const MAX_RUN_JS_ARTIFACT_COUNT = 32;
4
7
  class BrowserRuntime {
5
8
  constructor(context, downloadsPath) {
6
9
  this.context = context;
@@ -239,17 +242,46 @@ class BrowserRuntime {
239
242
  return { tabId, characters: input.text.length };
240
243
  case "run_js": {
241
244
  if (typeof input.code !== "string") throw new Error("run_js requires code.");
242
- const result = await page.evaluate(async (code) => {
243
- const invoke = new Function(`"use strict"; return (async () => {
245
+ const evaluated = await page.evaluate(
246
+ async ({ code, maxBytes, maxCount }) => {
247
+ const artifacts = [];
248
+ let totalBytes = 0;
249
+ const r5dCreateArtifact = (filename, content) => {
250
+ if (typeof filename !== "string" || !filename.trim()) {
251
+ throw new TypeError("r5dCreateArtifact(filename, content): filename must be a non-empty string.");
252
+ }
253
+ if (typeof content !== "string") {
254
+ throw new TypeError(
255
+ `r5dCreateArtifact(${JSON.stringify(filename)}, content): content must be a string. Use JSON.stringify(value) for objects.`
256
+ );
257
+ }
258
+ if (artifacts.length >= maxCount) {
259
+ throw new RangeError(`r5dCreateArtifact: at most ${maxCount} artifacts can be created in one browser_run_js call.`);
260
+ }
261
+ totalBytes += new TextEncoder().encode(content).length;
262
+ if (totalBytes > maxBytes) {
263
+ throw new RangeError(`r5dCreateArtifact: artifact content exceeded the ${maxBytes} byte budget for one browser_run_js call.`);
264
+ }
265
+ artifacts.push({ filename, content });
266
+ return filename;
267
+ };
268
+ const invoke = new Function("r5dCreateArtifact", `"use strict"; return (async () => {
244
269
  ${code}
245
270
  })()`);
246
- return await invoke();
247
- }, input.code);
248
- const serialized = JSON.stringify(result);
249
- if (serialized && Buffer.byteLength(serialized) > 1024 * 1024) {
271
+ try {
272
+ return { result: await invoke(r5dCreateArtifact), artifacts };
273
+ } catch (error) {
274
+ if (artifacts.length === 0) throw error;
275
+ return { artifacts, failure: error instanceof Error ? error.message : String(error) };
276
+ }
277
+ },
278
+ { code: input.code, maxBytes: MAX_RUN_JS_ARTIFACT_BYTES, maxCount: MAX_RUN_JS_ARTIFACT_COUNT }
279
+ );
280
+ const serialized = JSON.stringify(evaluated.result);
281
+ if (serialized && Buffer.byteLength(serialized) > MAX_RUN_JS_RESULT_BYTES) {
250
282
  throw new Error("Browser JavaScript result exceeded the 1 MiB limit.");
251
283
  }
252
- return { tabId, result };
284
+ return { tabId, result: evaluated.result, artifacts: evaluated.artifacts, failure: evaluated.failure };
253
285
  }
254
286
  default:
255
287
  throw new Error(`Unsupported browser operation: ${action}`);
package/dist/mjs/main.mjs CHANGED
@@ -127,7 +127,7 @@ async function connectLoop(params) {
127
127
  version: findVersion(),
128
128
  chromiumVersion: params.chromiumVersion,
129
129
  profilePath: params.profilePath,
130
- capabilities: { portForwarding: true }
130
+ capabilities: { portForwarding: true, runJsArtifacts: true }
131
131
  },
132
132
  tabs: await params.runtime.listTabs(),
133
133
  portForwards: params.portForwards.list()
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "type": "module"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-browser",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",