chromiumfish 0.2.0 → 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 +21 -6
- package/dist/agent.d.ts +8 -3
- package/dist/agent.js +7 -6
- package/dist/cli.js +147 -0
- package/dist/version.d.ts +3 -3
- package/dist/version.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,13 +62,11 @@ and connects over CDP; `runTask` drives it from a plain-language goal.
|
|
|
62
62
|
```ts
|
|
63
63
|
import { withAgent } from "chromiumfish";
|
|
64
64
|
|
|
65
|
-
// LLM config
|
|
66
|
-
const
|
|
67
|
-
agent
|
|
68
|
-
.runTask("Search DuckDuckGo for 'chromiumfish' and give me the first result's URL.")
|
|
69
|
-
.then((r) => r.finalText),
|
|
65
|
+
// LLM config: a nearby .env (OPENAI_API_*), or pass apiKey/apiBase/model here.
|
|
66
|
+
const result = await withAgent({ typing: "human" }, (agent) =>
|
|
67
|
+
agent.runTask("Search DuckDuckGo for 'chromiumfish' and give me the first result's URL."),
|
|
70
68
|
);
|
|
71
|
-
console.log(
|
|
69
|
+
console.log(result.finalText);
|
|
72
70
|
```
|
|
73
71
|
|
|
74
72
|
`withAgent` shuts the browser down for you; use `launchAgent` directly if you
|
|
@@ -78,6 +76,7 @@ want to manage the lifecycle (`const { agent, close } = await launchAgent()`).
|
|
|
78
76
|
|--------|---------|-------------|
|
|
79
77
|
| `typing` | `"human"` | Typing speed: `"human"` (~75 WPM, natural), `"fast"`, `"instant"`, or a custom `[keyDown, keyUp, longMultiplier]` triple (numbers = ms). |
|
|
80
78
|
| `model` | env | Model for the session (overrides `OPENAI_API_MODEL`); `runTask({ model })` overrides per task. |
|
|
79
|
+
| `apiKey` / `apiBase` | env | LLM key / base URL (override `OPENAI_API_KEY` / `OPENAI_API_BASE`). |
|
|
81
80
|
| `chrome` | `CHROME_BIN` / cached build | Path to the ChromiumFish binary. |
|
|
82
81
|
| `port` | `9222` | DevTools remote-debugging port. |
|
|
83
82
|
| `extraArgs` | — | Extra Chromium flags. |
|
|
@@ -85,11 +84,27 @@ want to manage the lifecycle (`const { agent, close } = await launchAgent()`).
|
|
|
85
84
|
> Needs a WebSocket: the Node 22+ global `WebSocket` is used automatically; on
|
|
86
85
|
> Node <22 install the optional `ws` package (`npm install ws`).
|
|
87
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
|
+
|
|
88
100
|
## CLI
|
|
89
101
|
|
|
90
102
|
```bash
|
|
91
103
|
npx chromiumfish fetch [--browser-version X] [--force] # download + cache
|
|
92
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]
|
|
93
108
|
npx chromiumfish clear # wipe the cache
|
|
94
109
|
npx chromiumfish --version
|
|
95
110
|
```
|
package/dist/agent.d.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface LaunchAgentOptions {
|
|
|
52
52
|
port?: number;
|
|
53
53
|
/** Path to the ChromiumFish binary; defaults to CHROME_BIN env or the cached build. */
|
|
54
54
|
chrome?: string;
|
|
55
|
+
/** LLM API key (overrides OPENAI_API_KEY). */
|
|
56
|
+
apiKey?: string;
|
|
57
|
+
/** LLM base URL (overrides OPENAI_API_BASE). */
|
|
58
|
+
apiBase?: string;
|
|
55
59
|
/** Model for this session (overrides OPENAI_API_MODEL). */
|
|
56
60
|
model?: string;
|
|
57
61
|
/** Typing cadence: "human" (default), "fast", "instant", or a [keyDown, keyUp, multiplier] triple. */
|
|
@@ -72,9 +76,10 @@ export interface AgentSession {
|
|
|
72
76
|
/**
|
|
73
77
|
* Launch a local ChromiumFish with the AI agent layer and connect to it.
|
|
74
78
|
*
|
|
75
|
-
* LLM config
|
|
76
|
-
* (a nearby .env is loaded
|
|
77
|
-
*
|
|
79
|
+
* LLM config can be passed in-script (`apiKey` / `apiBase` / `model`) or left to
|
|
80
|
+
* OPENAI_API_KEY / OPENAI_API_BASE / OPENAI_API_MODEL (a nearby .env is loaded
|
|
81
|
+
* automatically); an explicit option wins over the env var. Prefer {@link withAgent}
|
|
82
|
+
* for automatic cleanup, or remember to call the returned `close()`.
|
|
78
83
|
*/
|
|
79
84
|
export declare function launchAgent(opts?: LaunchAgentOptions): Promise<AgentSession>;
|
|
80
85
|
/**
|
package/dist/agent.js
CHANGED
|
@@ -350,12 +350,13 @@ function loadDotenv() {
|
|
|
350
350
|
/**
|
|
351
351
|
* Launch a local ChromiumFish with the AI agent layer and connect to it.
|
|
352
352
|
*
|
|
353
|
-
* LLM config
|
|
354
|
-
* (a nearby .env is loaded
|
|
355
|
-
*
|
|
353
|
+
* LLM config can be passed in-script (`apiKey` / `apiBase` / `model`) or left to
|
|
354
|
+
* OPENAI_API_KEY / OPENAI_API_BASE / OPENAI_API_MODEL (a nearby .env is loaded
|
|
355
|
+
* automatically); an explicit option wins over the env var. Prefer {@link withAgent}
|
|
356
|
+
* for automatic cleanup, or remember to call the returned `close()`.
|
|
356
357
|
*/
|
|
357
358
|
export async function launchAgent(opts = {}) {
|
|
358
|
-
const { port = 9222, model = "", typing = "human", loadDotenv: doDotenv = true, extraArgs = [], timeoutMs = 30_000 } = opts;
|
|
359
|
+
const { port = 9222, apiKey = "", apiBase = "", model = "", typing = "human", loadDotenv: doDotenv = true, extraArgs = [], timeoutMs = 30_000 } = opts;
|
|
359
360
|
if (doDotenv)
|
|
360
361
|
loadDotenv();
|
|
361
362
|
let chrome = opts.chrome ?? process.env.CHROME_BIN;
|
|
@@ -372,8 +373,8 @@ export async function launchAgent(opts = {}) {
|
|
|
372
373
|
typingFlag(typing),
|
|
373
374
|
"--no-first-run",
|
|
374
375
|
"--no-default-browser-check",
|
|
375
|
-
`--agent-llm-url=${process.env.OPENAI_API_BASE ?? ""}`,
|
|
376
|
-
`--agent-llm-key=${process.env.OPENAI_API_KEY ?? ""}`,
|
|
376
|
+
`--agent-llm-url=${apiBase || (process.env.OPENAI_API_BASE ?? "")}`,
|
|
377
|
+
`--agent-llm-key=${apiKey || (process.env.OPENAI_API_KEY ?? "")}`,
|
|
377
378
|
`--agent-model=${model || (process.env.OPENAI_API_MODEL ?? "")}`,
|
|
378
379
|
...extraArgs,
|
|
379
380
|
];
|
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,9 +6,9 @@
|
|
|
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.
|
|
9
|
+
export declare const SDK_VERSION = "0.2.2";
|
|
10
10
|
/** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
|
|
11
|
-
export declare const DEFAULT_BROWSER_VERSION = "
|
|
11
|
+
export declare const DEFAULT_BROWSER_VERSION = "149.0.7827.115";
|
|
12
12
|
/** Public repo hosting the release assets. */
|
|
13
13
|
export declare const RELEASE_REPO = "arman-bd/chromiumfish";
|
|
14
14
|
/**
|
|
@@ -30,7 +30,7 @@ export declare const GEOIP_FALLBACK_VERSION = "2026.06";
|
|
|
30
30
|
* Reject version strings that aren't a plain build tag. Versions are
|
|
31
31
|
* interpolated into filesystem cache paths and release URLs, so a crafted
|
|
32
32
|
* value like `../../../etc` would escape the cache dir (path traversal).
|
|
33
|
-
* Real tags are digits, dots, and hyphens (e.g. "
|
|
33
|
+
* Real tags are digits, dots, and hyphens (e.g. "149.0.7827.115", "2026.06",
|
|
34
34
|
* "latest").
|
|
35
35
|
*/
|
|
36
36
|
export declare function assertSafeVersion(version: string): string;
|
package/dist/version.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
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.
|
|
9
|
+
export const SDK_VERSION = "0.2.2";
|
|
10
10
|
/** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
|
|
11
|
-
export const DEFAULT_BROWSER_VERSION = "
|
|
11
|
+
export const DEFAULT_BROWSER_VERSION = "149.0.7827.115";
|
|
12
12
|
/** Public repo hosting the release assets. */
|
|
13
13
|
export const RELEASE_REPO = "arman-bd/chromiumfish";
|
|
14
14
|
/**
|
|
@@ -30,7 +30,7 @@ export const GEOIP_FALLBACK_VERSION = "2026.06";
|
|
|
30
30
|
* Reject version strings that aren't a plain build tag. Versions are
|
|
31
31
|
* interpolated into filesystem cache paths and release URLs, so a crafted
|
|
32
32
|
* value like `../../../etc` would escape the cache dir (path traversal).
|
|
33
|
-
* Real tags are digits, dots, and hyphens (e.g. "
|
|
33
|
+
* Real tags are digits, dots, and hyphens (e.g. "149.0.7827.115", "2026.06",
|
|
34
34
|
* "latest").
|
|
35
35
|
*/
|
|
36
36
|
export function assertSafeVersion(version) {
|
package/package.json
CHANGED