chromiumfish 0.2.1 → 0.2.2

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
@@ -84,11 +84,27 @@ want to manage the lifecycle (`const { agent, close } = await launchAgent()`).
84
84
  > Needs a WebSocket: the Node 22+ global `WebSocket` is used automatically; on
85
85
  > Node <22 install the optional `ws` package (`npm install ws`).
86
86
 
87
+ ## External agents
88
+
89
+ Prefer a third-party framework (Hermes, OpenClaw, browser-use, Playwright, …)? `chromiumfish
90
+ serve` exposes a plain CDP endpoint — with your persona/proxy/timezone active — for any of them
91
+ to attach to:
92
+
93
+ ```bash
94
+ npx chromiumfish serve --persona-seed alice # -> http://127.0.0.1:9222
95
+ # e.g. Hermes ~/.hermes/config.yaml: browser: { cdp_url: "http://127.0.0.1:9222" }
96
+ ```
97
+
98
+ Full guide: [chromiumfish.com/agents](https://chromiumfish.com/agents).
99
+
87
100
  ## CLI
88
101
 
89
102
  ```bash
90
103
  npx chromiumfish fetch [--browser-version X] [--force] # download + cache
91
104
  npx chromiumfish path # print binary path
105
+ npx chromiumfish serve [--port 9222] [--persona-seed S] # CDP endpoint for external agents
106
+ [--proxy URL] [--window-size WxH] [--timezone Z] [--headless]
107
+ [--browser-version X] [--extra-args ARGS] [--timeout S]
92
108
  npx chromiumfish clear # wipe the cache
93
109
  npx chromiumfish --version
94
110
  ```
package/dist/cli.js CHANGED
@@ -1,7 +1,149 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import * as fs from "node:fs";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import * as path from "node:path";
3
7
  import { binaryPath, cacheRoot, fetchBrowser } from "./fetch.js";
8
+ import { buildArgs } from "./launcher.js";
9
+ import { resolveTimezone } from "./ip2tz.js";
4
10
  import { SDK_VERSION, browserVersion } from "./version.js";
11
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
12
+ /** Read `--flag value` from argv, or undefined. */
13
+ function flag(argv, name) {
14
+ const i = argv.indexOf(name);
15
+ return i >= 0 ? argv[i + 1] : undefined;
16
+ }
17
+ /**
18
+ * Launch ChromiumFish as a bare CDP endpoint for external agent frameworks
19
+ * (Hermes, OpenClaw, browser-use, ...) to attach to. Unlike `launchAgent`, this
20
+ * adds NO `--agent-*` switches — it just exposes Chrome DevTools Protocol with
21
+ * the persona/proxy/timezone you pick, then blocks until interrupted.
22
+ */
23
+ async function serve(argv) {
24
+ const port = Number(flag(argv, "--port") ?? 9222);
25
+ const personaSeed = flag(argv, "--persona-seed");
26
+ const proxy = flag(argv, "--proxy");
27
+ const windowSize = flag(argv, "--window-size") ?? "1920x1080";
28
+ const timezone = flag(argv, "--timezone");
29
+ const headless = argv.includes("--headless");
30
+ const version = flag(argv, "--browser-version");
31
+ const extraArgsRaw = flag(argv, "--extra-args");
32
+ const timeoutSecs = Number(flag(argv, "--timeout") ?? 30);
33
+ // Validate up front — Python's argparse does this for us; here it's manual.
34
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
35
+ console.error(`invalid --port ${flag(argv, "--port")}; expected an integer 1-65535`);
36
+ return 1;
37
+ }
38
+ if (!Number.isFinite(timeoutSecs) || timeoutSecs <= 0) {
39
+ console.error(`invalid --timeout ${flag(argv, "--timeout")}; expected a positive number of seconds`);
40
+ return 1;
41
+ }
42
+ const [w, h] = windowSize.toLowerCase().split("x").map(Number);
43
+ if (!w || !h) {
44
+ console.error(`invalid --window-size ${windowSize}; expected WIDTHxHEIGHT, e.g. 1920x1080`);
45
+ return 1;
46
+ }
47
+ const chrome = await binaryPath(version); // fetches if not cached
48
+ const profile = mkdtempSync(path.join(tmpdir(), "cf-serve-"));
49
+ const cleanup = () => rmSync(profile, { recursive: true, force: true });
50
+ let proc;
51
+ const killTree = (sig) => {
52
+ if (!proc?.pid)
53
+ return;
54
+ try {
55
+ process.kill(-proc.pid, sig); // negative pid = the whole process group
56
+ }
57
+ catch {
58
+ try {
59
+ proc.kill(sig);
60
+ }
61
+ catch {
62
+ /* already gone */
63
+ }
64
+ }
65
+ };
66
+ // try/finally guarantees the browser tree + temp profile are torn down even
67
+ // if resolveTimezone/spawn throws or an early return fires — matches the
68
+ // Python handler's finally -> _teardown().
69
+ try {
70
+ const extra = [];
71
+ if (headless)
72
+ extra.push("--headless=new");
73
+ if (proxy)
74
+ extra.push(`--proxy-server=${proxy}`);
75
+ if (extraArgsRaw)
76
+ extra.push(...extraArgsRaw.split(",").filter(Boolean));
77
+ const args = [
78
+ `--remote-debugging-port=${port}`,
79
+ // External clients send an Origin header DevTools rejects unless allowed.
80
+ "--remote-allow-origins=*",
81
+ `--user-data-dir=${profile}`,
82
+ "--no-first-run",
83
+ "--no-default-browser-check",
84
+ ...buildArgs({ personaSeed, windowSize: [w, h], args: extra }),
85
+ ];
86
+ const env = { ...process.env };
87
+ if (timezone) {
88
+ const tz = timezone === "auto" ? await resolveTimezone({ proxy }) : timezone;
89
+ if (tz)
90
+ env.TZ = tz;
91
+ }
92
+ const base = `http://127.0.0.1:${port}`;
93
+ // detached: own process group, so we can signal the WHOLE tree (browser +
94
+ // GPU/renderer/network helpers) on exit instead of leaking the helpers.
95
+ proc = spawn(chrome, args, { stdio: "ignore", env, detached: true });
96
+ const deadline = Date.now() + timeoutSecs * 1000;
97
+ let ver = null;
98
+ for (;;) {
99
+ try {
100
+ const r = await fetch(`${base}/json/version`);
101
+ if (r.ok) {
102
+ ver = (await r.json());
103
+ break;
104
+ }
105
+ }
106
+ catch {
107
+ /* not up yet */
108
+ }
109
+ if (proc.exitCode !== null) {
110
+ console.error("ChromiumFish exited before the CDP endpoint came up");
111
+ return 1;
112
+ }
113
+ if (Date.now() > deadline) {
114
+ console.error("ChromiumFish did not expose its CDP endpoint in time");
115
+ return 1;
116
+ }
117
+ await sleep(400);
118
+ }
119
+ console.log(`ChromiumFish ${ver?.Browser ?? ""} ready — CDP endpoint for external agents`);
120
+ console.log(` HTTP : ${base} (discovery: ${base}/json/version)`);
121
+ if (ver?.webSocketDebuggerUrl)
122
+ console.log(` WS : ${ver.webSocketDebuggerUrl}`);
123
+ console.log("");
124
+ console.log("Attach an agent, e.g.:");
125
+ console.log(` Hermes ~/.hermes/config.yaml -> browser: { cdp_url: "${base}" }`);
126
+ console.log(` browser-use BrowserSession(cdp_url="${base}")`);
127
+ console.log(` OpenClaw profile cdpUrl: "${base}"`);
128
+ console.log("Ctrl-C to stop.");
129
+ await new Promise((resolve) => {
130
+ const stop = () => {
131
+ console.log("\nstopping…");
132
+ killTree("SIGTERM");
133
+ resolve();
134
+ };
135
+ process.on("SIGINT", stop);
136
+ process.on("SIGTERM", stop);
137
+ proc.on("exit", () => resolve());
138
+ });
139
+ return 0;
140
+ }
141
+ finally {
142
+ await sleep(300);
143
+ killTree("SIGKILL");
144
+ cleanup();
145
+ }
146
+ }
5
147
  async function main(argv) {
6
148
  const cmd = argv[2];
7
149
  switch (cmd) {
@@ -26,6 +168,8 @@ async function main(argv) {
26
168
  }
27
169
  return 0;
28
170
  }
171
+ case "serve":
172
+ return await serve(argv);
29
173
  case "--version":
30
174
  case "-V":
31
175
  console.log(`chromiumfish ${SDK_VERSION} (browser ${browserVersion()})`);
@@ -37,6 +181,9 @@ async function main(argv) {
37
181
  "Usage:",
38
182
  " chromiumfish fetch [--browser-version X] [--force] download + cache",
39
183
  " chromiumfish path print binary path",
184
+ " chromiumfish serve [--port 9222] [--persona-seed S] CDP endpoint for agents",
185
+ " [--proxy URL] [--window-size WxH] [--timezone Z] [--headless]",
186
+ " [--browser-version X] [--extra-args ARGS] [--timeout S]",
40
187
  " chromiumfish clear wipe the cache",
41
188
  " chromiumfish --version",
42
189
  ].join("\n"));
package/dist/version.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
7
7
  */
8
8
  /** SDK package version (kept in sync with package.json). */
9
- export declare const SDK_VERSION = "0.2.1";
9
+ export declare const SDK_VERSION = "0.2.2";
10
10
  /** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
11
11
  export declare const DEFAULT_BROWSER_VERSION = "149.0.7827.115";
12
12
  /** Public repo hosting the release assets. */
package/dist/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
7
7
  */
8
8
  /** SDK package version (kept in sync with package.json). */
9
- export const SDK_VERSION = "0.2.1";
9
+ export const SDK_VERSION = "0.2.2";
10
10
  /** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
11
11
  export const DEFAULT_BROWSER_VERSION = "149.0.7827.115";
12
12
  /** Public repo hosting the release assets. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromiumfish",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Stealth Chromium build with a drop-in Playwright harness — fetches and launches the ChromiumFish browser.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",