@flrande/browserctl 0.2.0 → 0.3.0

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/INSTALL-CN.md ADDED
@@ -0,0 +1,92 @@
1
+ # 安装 browserctl Skills
2
+
3
+ [English](INSTALL.md) | 简体中文
4
+
5
+ 本仓库发布一个默认可安装 skill:
6
+
7
+ - `skills/browserctl`
8
+
9
+ ## 给 Agent 的主提示词
10
+
11
+ 把下面文本贴给 agent:
12
+
13
+ ```text
14
+ 请抓取并遵循 https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md 中的安装说明。
15
+ ```
16
+
17
+ ## 平台安装入口
18
+
19
+ - Codex:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
20
+ - Claude Code:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
21
+ - Cursor:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
22
+ - OpenCode:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
23
+
24
+ ## 安装后更新
25
+
26
+ 默认推荐使用“仓库同源更新”,保证 skill、扩展和 CLI 版本一致。
27
+
28
+ PowerShell 7:
29
+
30
+ ```powershell
31
+ $repoPath = Join-Path $env:USERPROFILE ".codex\\browserctl" # 按你的本地克隆路径调整
32
+
33
+ git -C $repoPath pull --ff-only
34
+ npm install --global "$repoPath"
35
+
36
+ browserctl daemon-stop --json
37
+ browserctl daemon-start --json
38
+ ```
39
+
40
+ Bash:
41
+
42
+ ```bash
43
+ repoPath="$HOME/.codex/browserctl" # 按你的本地克隆路径调整
44
+
45
+ git -C "$repoPath" pull --ff-only
46
+ npm install --global "$repoPath"
47
+
48
+ browserctl daemon-stop --json
49
+ browserctl daemon-start --json
50
+ ```
51
+
52
+ 拉取后需要在 `edge://extensions` 或 `chrome://extensions` 里刷新扩展:
53
+
54
+ 1. 打开扩展管理页。
55
+ 2. 找到 `BrowserCtl Relay`。
56
+ 3. 点击 `Reload`。
57
+
58
+ 验证命令:
59
+
60
+ PowerShell 7:
61
+
62
+ ```powershell
63
+ browserctl status --json --session relay --profile chrome-relay
64
+ Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
65
+ ```
66
+
67
+ Bash:
68
+
69
+ ```bash
70
+ browserctl status --json --session relay --profile chrome-relay
71
+ curl --fail --max-time 3 "http://127.0.0.1:9223/browserctl/relay/status"
72
+ ```
73
+
74
+ 可选兜底(非默认):仅从 npm 仓库更新 CLI。
75
+
76
+ PowerShell 7:
77
+
78
+ ```powershell
79
+ npm install --global @flrande/browserctl@latest
80
+ ```
81
+
82
+ Bash:
83
+
84
+ ```bash
85
+ npm install --global @flrande/browserctl@latest
86
+ ```
87
+
88
+ ## 兜底约定
89
+
90
+ 如果平台安装入口不可用,agent 应直接抓取并遵循:
91
+
92
+ - `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/skills/browserctl/SKILL.md`
package/INSTALL.md ADDED
@@ -0,0 +1,92 @@
1
+ # Install browserctl Skills
2
+
3
+ English | [简体中文](INSTALL-CN.md)
4
+
5
+ This repository publishes one default installable skill:
6
+
7
+ - `skills/browserctl`
8
+
9
+ ## Primary Agent Prompt
10
+
11
+ Paste this to your agent:
12
+
13
+ ```text
14
+ Fetch and follow instructions from https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md
15
+ ```
16
+
17
+ ## Platform Guides
18
+
19
+ - Codex: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
20
+ - Claude Code: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
21
+ - Cursor: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
22
+ - OpenCode: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
23
+
24
+ ## Update After Install
25
+
26
+ Recommended default: use repository-synced updates so skill, extension, and CLI stay aligned.
27
+
28
+ PowerShell 7:
29
+
30
+ ```powershell
31
+ $repoPath = Join-Path $env:USERPROFILE ".codex\\browserctl" # adjust to your local clone path
32
+
33
+ git -C $repoPath pull --ff-only
34
+ npm install --global "$repoPath"
35
+
36
+ browserctl daemon-stop --json
37
+ browserctl daemon-start --json
38
+ ```
39
+
40
+ Bash:
41
+
42
+ ```bash
43
+ repoPath="$HOME/.codex/browserctl" # adjust to your local clone path
44
+
45
+ git -C "$repoPath" pull --ff-only
46
+ npm install --global "$repoPath"
47
+
48
+ browserctl daemon-stop --json
49
+ browserctl daemon-start --json
50
+ ```
51
+
52
+ Reload the extension in `edge://extensions` or `chrome://extensions` after pulling:
53
+
54
+ 1. Open extension management page.
55
+ 2. Find `BrowserCtl Relay`.
56
+ 3. Click `Reload`.
57
+
58
+ Verify:
59
+
60
+ PowerShell 7:
61
+
62
+ ```powershell
63
+ browserctl status --json --session relay --profile chrome-relay
64
+ Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
65
+ ```
66
+
67
+ Bash:
68
+
69
+ ```bash
70
+ browserctl status --json --session relay --profile chrome-relay
71
+ curl --fail --max-time 3 "http://127.0.0.1:9223/browserctl/relay/status"
72
+ ```
73
+
74
+ Optional fallback (not the default): update CLI from npm registry only.
75
+
76
+ PowerShell 7:
77
+
78
+ ```powershell
79
+ npm install --global @flrande/browserctl@latest
80
+ ```
81
+
82
+ Bash:
83
+
84
+ ```bash
85
+ npm install --global @flrande/browserctl@latest
86
+ ```
87
+
88
+ ## Fallback Contract
89
+
90
+ If platform-specific install is unavailable, agents should fetch and follow:
91
+
92
+ - `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/skills/browserctl/SKILL.md`
package/README-CN.md CHANGED
@@ -2,65 +2,68 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- 最快路径:通过 relay 扩展直接控制你已打开的 Edge/Chrome。
5
+ 面向 coding agent 的扩展优先浏览器控制工具集。
6
6
 
7
- ## 扩展模式最简上手(推荐)
7
+ ## 给 Agent 安装
8
8
 
9
- 前置条件:
9
+ 把下面文本贴给 agent:
10
10
 
11
- - Node.js 22+
12
- - npm 或 pnpm
13
- - PowerShell 7 (`pwsh`)
14
- - Edge 或 Chrome
11
+ ```text
12
+ 请抓取并遵循 https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md 中的说明。
13
+ ```
15
14
 
16
- 1. 安装已发布包:
15
+ 平台快捷入口:
17
16
 
18
- ```powershell
19
- npm install --global @flrande/browserctl
20
- # 或:pnpm add --global @flrande/browserctl
21
- ```
17
+ - Codex:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
18
+ - Claude Code:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
19
+ - Cursor:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
20
+ - OpenCode:`https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
21
+
22
+ 默认 skill:
23
+
24
+ - 名称:`browserctl`
25
+ - 路径:`skills/browserctl`
22
26
 
23
- 2. 用扩展 relay 模式启动 daemon:
27
+ ## 浏览器快速开始(扩展默认)
28
+
29
+ PowerShell 7:
24
30
 
25
31
  ```powershell
32
+ npm install --global @flrande/browserctl
33
+
26
34
  $env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
27
35
  $env:BROWSERD_CHROME_RELAY_MODE = "extension"
28
36
  $env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
29
- $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay-secret"
37
+ $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay"
38
+
30
39
  browserctl daemon-stop --json
31
40
  browserctl daemon-start --json
41
+ browserctl status --json --session relay --profile chrome-relay
32
42
  ```
33
43
 
34
- 3. 加载扩展:
44
+ Bash:
35
45
 
36
- 1. 打开 `edge://extensions` 或 `chrome://extensions`。
37
- 2. 开启开发者模式。
38
- 3. 选择“加载已解压的扩展程序”,目录为 `extensions/chrome-relay`。
39
- 4. 在扩展弹窗中设置:
40
- - `Bridge URL`: `ws://127.0.0.1:9223/bridge`
41
- - `Token`: `relay-secret`
42
- 5. 点击 `Save`,再点击 `Reconnect`。
46
+ ```bash
47
+ npm install --global @flrande/browserctl
43
48
 
44
- 4. 连接验证与首个命令:
49
+ export BROWSERD_DEFAULT_DRIVER="chrome-relay"
50
+ export BROWSERD_CHROME_RELAY_MODE="extension"
51
+ export BROWSERD_CHROME_RELAY_URL="http://127.0.0.1:9223"
52
+ export BROWSERD_CHROME_RELAY_EXTENSION_TOKEN="browserctl-relay"
45
53
 
46
- ```powershell
54
+ browserctl daemon-stop --json
55
+ browserctl daemon-start --json
47
56
  browserctl status --json --session relay --profile chrome-relay
48
- browserctl tab-open --json --session relay --profile chrome-relay https://example.com
49
- ```
50
-
51
- ## 30 秒排查
52
-
53
- ```powershell
54
- browserctl daemon-status --json
55
- Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
56
57
  ```
57
58
 
58
- 若 `connected` 为 `false`,重新打开扩展弹窗并点击 `Reconnect`。
59
-
60
59
  ## 完整文档
61
60
 
62
- - [Full User Guide (English)](docs/user-guide.md)
63
- - [完整使用文档(中文)](docs/user-guide-cn.md)
64
- - [Chrome Relay Extension README (English)](extensions/chrome-relay/README.md)
65
- - [Chrome Relay 扩展文档(中文)](extensions/chrome-relay/README-CN.md)
66
-
61
+ - [Install Guide (English)](INSTALL.md)
62
+ - [安装指南(中文)](INSTALL-CN.md)
63
+ - [User Guide (English)](docs/user-guide.md)
64
+ - [使用指南(中文)](docs/user-guide-cn.md)
65
+ - 安装后更新(skill + 扩展 + CLI):[使用指南](docs/user-guide-cn.md)、[CLI 参考](docs/cli-reference-cn.md)
66
+ - [CLI Reference (English)](docs/cli-reference.md)
67
+ - [CLI 参考(中文)](docs/cli-reference-cn.md)
68
+ - [Maintainer Onboarding (English)](docs/maintainer-onboarding.md)
69
+ - [维护者上手文档(中文)](docs/maintainer-onboarding-cn.md)
package/README.md CHANGED
@@ -2,65 +2,68 @@
2
2
 
3
3
  English | [简体中文](README-CN.md)
4
4
 
5
- Fastest path: control your current Edge/Chrome through the relay extension.
5
+ Extension-first browser control toolkit for coding agents.
6
6
 
7
- ## Extension Quick Start (Recommended)
7
+ ## Install In Agents
8
8
 
9
- Prerequisites:
9
+ Paste this to your agent:
10
10
 
11
- - Node.js 22+
12
- - npm or pnpm
13
- - PowerShell 7 (`pwsh`)
14
- - Edge or Chrome
11
+ ```text
12
+ Fetch and follow instructions from https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/INSTALL.md
13
+ ```
15
14
 
16
- 1. Install the released package:
15
+ Platform shortcuts:
17
16
 
18
- ```powershell
19
- npm install --global @flrande/browserctl
20
- # or: pnpm add --global @flrande/browserctl
21
- ```
17
+ - Codex: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.codex/INSTALL.md`
18
+ - Claude Code: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.claude-plugin/INSTALL.md`
19
+ - Cursor: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.cursor-plugin/INSTALL.md`
20
+ - OpenCode: `https://raw.githubusercontent.com/Flrande/browserctl/refs/heads/master/.opencode/INSTALL.md`
21
+
22
+ Default skill:
23
+
24
+ - Name: `browserctl`
25
+ - Path: `skills/browserctl`
22
26
 
23
- 2. Start daemon in extension relay mode:
27
+ ## Browser Quick Start (Extension Default)
28
+
29
+ PowerShell 7:
24
30
 
25
31
  ```powershell
32
+ npm install --global @flrande/browserctl
33
+
26
34
  $env:BROWSERD_DEFAULT_DRIVER = "chrome-relay"
27
35
  $env:BROWSERD_CHROME_RELAY_MODE = "extension"
28
36
  $env:BROWSERD_CHROME_RELAY_URL = "http://127.0.0.1:9223"
29
- $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "relay-secret"
37
+ $env:BROWSERD_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay"
38
+
30
39
  browserctl daemon-stop --json
31
40
  browserctl daemon-start --json
41
+ browserctl status --json --session relay --profile chrome-relay
32
42
  ```
33
43
 
34
- 3. Load extension:
44
+ Bash:
35
45
 
36
- 1. Open `edge://extensions` or `chrome://extensions`.
37
- 2. Enable Developer mode.
38
- 3. Load unpacked extension from `extensions/chrome-relay`.
39
- 4. In extension popup, set:
40
- - `Bridge URL`: `ws://127.0.0.1:9223/bridge`
41
- - `Token`: `relay-secret`
42
- 5. Click `Save` then `Reconnect`.
46
+ ```bash
47
+ npm install --global @flrande/browserctl
43
48
 
44
- 4. Verify and run first command:
49
+ export BROWSERD_DEFAULT_DRIVER="chrome-relay"
50
+ export BROWSERD_CHROME_RELAY_MODE="extension"
51
+ export BROWSERD_CHROME_RELAY_URL="http://127.0.0.1:9223"
52
+ export BROWSERD_CHROME_RELAY_EXTENSION_TOKEN="browserctl-relay"
45
53
 
46
- ```powershell
54
+ browserctl daemon-stop --json
55
+ browserctl daemon-start --json
47
56
  browserctl status --json --session relay --profile chrome-relay
48
- browserctl tab-open --json --session relay --profile chrome-relay https://example.com
49
- ```
50
-
51
- ## 30-Second Troubleshooting
52
-
53
- ```powershell
54
- browserctl daemon-status --json
55
- Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:9223/browserctl/relay/status" -TimeoutSec 3
56
57
  ```
57
58
 
58
- If `connected` is `false`, reopen extension popup and click `Reconnect`.
59
-
60
59
  ## Full Docs
61
60
 
62
- - [Full User Guide (English)](docs/user-guide.md)
63
- - [完整使用文档(中文)](docs/user-guide-cn.md)
64
- - [Chrome Relay Extension README (English)](extensions/chrome-relay/README.md)
65
- - [Chrome Relay 扩展文档(中文)](extensions/chrome-relay/README-CN.md)
66
-
61
+ - [Install Guide (English)](INSTALL.md)
62
+ - [安装指南(中文)](INSTALL-CN.md)
63
+ - [User Guide (English)](docs/user-guide.md)
64
+ - [使用指南(中文)](docs/user-guide-cn.md)
65
+ - Update after install (skill + extension + CLI): [User Guide](docs/user-guide.md), [CLI Reference](docs/cli-reference.md)
66
+ - [CLI Reference (English)](docs/cli-reference.md)
67
+ - [CLI 参考(中文)](docs/cli-reference-cn.md)
68
+ - [Maintainer Onboarding (English)](docs/maintainer-onboarding.md)
69
+ - [维护者上手文档(中文)](docs/maintainer-onboarding-cn.md)
@@ -2,9 +2,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
2
 
3
3
  import { stopDaemon } from "./daemon-client";
4
4
  import { EXIT_CODES, runCli } from "./main";
5
+ import { reserveLoopbackPort } from "./test-port";
5
6
 
6
- const TEST_DAEMON_PORT = "42491";
7
7
  const TEST_SESSION_ID = "session:e2e";
8
+ let testDaemonPort = 0;
8
9
 
9
10
  function createIoCapture() {
10
11
  const state = {
@@ -34,17 +35,20 @@ function parseJsonLine(state: { stdout: string }): Record<string, unknown> {
34
35
  }
35
36
 
36
37
  beforeEach(async () => {
37
- process.env.BROWSERCTL_DAEMON_PORT = TEST_DAEMON_PORT;
38
+ testDaemonPort = await reserveLoopbackPort();
39
+ process.env.BROWSERCTL_DAEMON_PORT = String(testDaemonPort);
38
40
  process.env.BROWSERD_MANAGED_LOCAL_ENABLED = "false";
39
41
  process.env.BROWSERD_DEFAULT_DRIVER = "managed";
40
- await stopDaemon(Number.parseInt(TEST_DAEMON_PORT, 10));
42
+ process.env.BROWSERD_CHROME_RELAY_MODE = "cdp";
43
+ await stopDaemon(testDaemonPort);
41
44
  });
42
45
 
43
46
  afterEach(async () => {
44
- await stopDaemon(Number.parseInt(TEST_DAEMON_PORT, 10));
47
+ await stopDaemon(testDaemonPort);
45
48
  delete process.env.BROWSERCTL_DAEMON_PORT;
46
49
  delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
47
50
  delete process.env.BROWSERD_DEFAULT_DRIVER;
51
+ delete process.env.BROWSERD_CHROME_RELAY_MODE;
48
52
  });
49
53
 
50
54
  describe("browserctl e2e", () => {
@@ -57,7 +61,7 @@ describe("browserctl e2e", () => {
57
61
  expect(startPayload.ok).toBe(true);
58
62
  const startData = startPayload.data as Record<string, unknown>;
59
63
  expect(startData.running).toBe(true);
60
- expect(startData.port).toBe(Number.parseInt(TEST_DAEMON_PORT, 10));
64
+ expect(startData.port).toBe(testDaemonPort);
61
65
 
62
66
  const openCapture = createIoCapture();
63
67
  const openExitCode = await runCli(
@@ -0,0 +1,97 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+
3
+ import { stopDaemon } from "./daemon-client";
4
+ import { EXIT_CODES, runCli } from "./main";
5
+ import { reserveLoopbackPort } from "./test-port";
6
+
7
+ let testDaemonPort = 0;
8
+ let activeRelayPort = 0;
9
+
10
+ function createIoCapture() {
11
+ const state = {
12
+ stdout: "",
13
+ stderr: ""
14
+ };
15
+
16
+ return {
17
+ state,
18
+ io: {
19
+ stdout: {
20
+ write(content: string) {
21
+ state.stdout += content;
22
+ }
23
+ },
24
+ stderr: {
25
+ write(content: string) {
26
+ state.stderr += content;
27
+ }
28
+ }
29
+ }
30
+ };
31
+ }
32
+
33
+ function parseJsonLine(state: { stdout: string }): Record<string, unknown> {
34
+ return JSON.parse(state.stdout.trim()) as Record<string, unknown>;
35
+ }
36
+
37
+ beforeEach(async () => {
38
+ testDaemonPort = await reserveLoopbackPort();
39
+ activeRelayPort = await reserveLoopbackPort();
40
+ while (activeRelayPort === testDaemonPort) {
41
+ activeRelayPort = await reserveLoopbackPort();
42
+ }
43
+
44
+ process.env.BROWSERCTL_DAEMON_PORT = String(testDaemonPort);
45
+ process.env.BROWSERD_CHROME_RELAY_URL = `http://127.0.0.1:${activeRelayPort}`;
46
+ delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
47
+ delete process.env.BROWSERD_DEFAULT_DRIVER;
48
+ delete process.env.BROWSERD_CHROME_RELAY_MODE;
49
+ delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
50
+ await stopDaemon(testDaemonPort);
51
+ });
52
+
53
+ afterEach(async () => {
54
+ await stopDaemon(testDaemonPort);
55
+ delete process.env.BROWSERCTL_DAEMON_PORT;
56
+ delete process.env.BROWSERD_CHROME_RELAY_URL;
57
+ delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
58
+ delete process.env.BROWSERD_DEFAULT_DRIVER;
59
+ delete process.env.BROWSERD_CHROME_RELAY_MODE;
60
+ delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
61
+ });
62
+
63
+ describe("browserctl smoke e2e", () => {
64
+ it("starts with extension-first defaults and reports chrome-relay status", async () => {
65
+ const startCapture = createIoCapture();
66
+ const startExitCode = await runCli(["daemon-start", "--json"], startCapture.io);
67
+
68
+ expect(startExitCode).toBe(EXIT_CODES.OK);
69
+ const startPayload = parseJsonLine(startCapture.state);
70
+ expect(startPayload.ok).toBe(true);
71
+
72
+ const statusCapture = createIoCapture();
73
+ const statusExitCode = await runCli(["status", "--json"], statusCapture.io);
74
+
75
+ expect(statusExitCode).toBe(EXIT_CODES.OK);
76
+ const statusPayload = parseJsonLine(statusCapture.state);
77
+ expect(statusPayload.ok).toBe(true);
78
+ const statusData = statusPayload.data as Record<string, unknown>;
79
+ expect(statusData.driver).toBe("chrome-relay");
80
+ expect(statusData.status).toMatchObject({
81
+ kind: "chrome-relay",
82
+ connected: false
83
+ });
84
+ const relayStatus = statusData.status as Record<string, unknown>;
85
+ expect(String(relayStatus.relayUrl ?? "")).toContain(`:${activeRelayPort}`);
86
+
87
+ const stopCapture = createIoCapture();
88
+ const stopExitCode = await runCli(["daemon-stop", "--json"], stopCapture.io);
89
+ expect(stopExitCode).toBe(EXIT_CODES.OK);
90
+ const stopPayload = parseJsonLine(stopCapture.state);
91
+ expect(stopPayload.ok).toBe(true);
92
+ expect(stopPayload.data).toMatchObject({
93
+ stopped: true,
94
+ port: testDaemonPort
95
+ });
96
+ }, 20_000);
97
+ });
@@ -0,0 +1,26 @@
1
+ import { createServer } from "node:net";
2
+
3
+ export async function reserveLoopbackPort(): Promise<number> {
4
+ return await new Promise<number>((resolve, reject) => {
5
+ const server = createServer();
6
+
7
+ server.once("error", reject);
8
+ server.listen(0, "127.0.0.1", () => {
9
+ const address = server.address();
10
+ if (typeof address !== "object" || address === null) {
11
+ server.close(() => reject(new Error("Failed to reserve loopback port.")));
12
+ return;
13
+ }
14
+
15
+ const port = address.port;
16
+ server.close((error) => {
17
+ if (error !== undefined) {
18
+ reject(error);
19
+ return;
20
+ }
21
+
22
+ resolve(port);
23
+ });
24
+ });
25
+ });
26
+ }
@@ -1,33 +1,8 @@
1
- import { createServer } from "node:net";
2
-
3
1
  import { afterEach, describe, expect, it } from "vitest";
4
2
  import { WebSocket } from "ws";
5
3
 
6
4
  import { createChromeRelayExtensionBridge } from "./chrome-relay-extension-bridge";
7
-
8
- async function reservePort(): Promise<number> {
9
- return await new Promise<number>((resolve, reject) => {
10
- const server = createServer();
11
- server.once("error", reject);
12
- server.listen(0, "127.0.0.1", () => {
13
- const address = server.address();
14
- if (typeof address !== "object" || address === null) {
15
- server.close(() => resolve(0));
16
- return;
17
- }
18
-
19
- const port = address.port;
20
- server.close((closeError) => {
21
- if (closeError !== undefined) {
22
- reject(closeError);
23
- return;
24
- }
25
-
26
- resolve(port);
27
- });
28
- });
29
- });
30
- }
5
+ import { reserveLoopbackPort } from "./test-port";
31
6
 
32
7
  async function waitForCondition(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
33
8
  const start = Date.now();
@@ -61,7 +36,7 @@ afterEach(async () => {
61
36
 
62
37
  describe("createChromeRelayExtensionBridge", () => {
63
38
  it("requires token configuration for extension bridge startup", async () => {
64
- const port = await reservePort();
39
+ const port = await reserveLoopbackPort();
65
40
 
66
41
  expect(() =>
67
42
  createChromeRelayExtensionBridge({
@@ -71,7 +46,7 @@ describe("createChromeRelayExtensionBridge", () => {
71
46
  });
72
47
 
73
48
  it("sends requests to extension client and receives responses", async () => {
74
- const port = await reservePort();
49
+ const port = await reserveLoopbackPort();
75
50
  const bridge = createChromeRelayExtensionBridge({
76
51
  relayUrl: `http://127.0.0.1:${port}`,
77
52
  token: "secret-token",
@@ -134,7 +109,7 @@ describe("createChromeRelayExtensionBridge", () => {
134
109
  });
135
110
 
136
111
  it("rejects invoke when extension is not connected", async () => {
137
- const port = await reservePort();
112
+ const port = await reserveLoopbackPort();
138
113
  const bridge = createChromeRelayExtensionBridge({
139
114
  relayUrl: `http://127.0.0.1:${port}`,
140
115
  token: "secret-token",
@@ -148,7 +123,7 @@ describe("createChromeRelayExtensionBridge", () => {
148
123
  });
149
124
 
150
125
  it("enforces token validation for extension websocket connection", async () => {
151
- const port = await reservePort();
126
+ const port = await reserveLoopbackPort();
152
127
  const bridge = createChromeRelayExtensionBridge({
153
128
  relayUrl: `http://127.0.0.1:${port}`,
154
129
  token: "secret-token",
@@ -188,7 +163,7 @@ describe("createChromeRelayExtensionBridge", () => {
188
163
  });
189
164
 
190
165
  it("forwards extension event envelopes to bridge listeners", async () => {
191
- const port = await reservePort();
166
+ const port = await reserveLoopbackPort();
192
167
  const bridge = createChromeRelayExtensionBridge({
193
168
  relayUrl: `http://127.0.0.1:${port}`,
194
169
  token: "secret-token",
@@ -53,9 +53,11 @@ export type BrowserdConfig = {
53
53
 
54
54
  const DEFAULT_DRIVER_KEY = "managed";
55
55
  const MANAGED_LOCAL_DRIVER_KEY = "managed-local";
56
+ const DEFAULT_CONFIG_DRIVER_KEY = "chrome-relay";
56
57
  const DEFAULT_MANAGED_LOCAL_BROWSER_NAME: ManagedLocalBrowserName = "chromium";
57
58
  const DEFAULT_MANAGED_LOCAL_HEADLESS = true;
58
- const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "cdp";
59
+ const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "extension";
60
+ const DEFAULT_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay";
59
61
  const DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS = 5_000;
60
62
  const DEFAULT_NETWORK_WAIT_TIMEOUT_MS = 10_000;
61
63
  const DEFAULT_NETWORK_WAIT_POLL_MS = 200;
@@ -181,7 +183,11 @@ function parseManagedLocalBrowserName(value: string | undefined): ManagedLocalBr
181
183
 
182
184
  function parseChromeRelayMode(value: string | undefined): ChromeRelayMode {
183
185
  const normalizedValue = value?.trim().toLowerCase();
184
- return normalizedValue === "extension" ? "extension" : DEFAULT_CHROME_RELAY_MODE;
186
+ if (normalizedValue === "extension" || normalizedValue === "cdp") {
187
+ return normalizedValue;
188
+ }
189
+
190
+ return DEFAULT_CHROME_RELAY_MODE;
185
191
  }
186
192
 
187
193
  export type BrowserdContainer = {
@@ -198,16 +204,12 @@ export function loadBrowserdConfig(
198
204
  ): BrowserdConfig {
199
205
  const managedLocalEnabled = parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_ENABLED, true);
200
206
  const chromeRelayMode = parseChromeRelayMode(env.BROWSERD_CHROME_RELAY_MODE);
201
- const chromeRelayExtensionToken = resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN);
202
- if (chromeRelayMode === "extension" && chromeRelayExtensionToken === undefined) {
203
- throw new Error(
204
- "BROWSERD_CHROME_RELAY_EXTENSION_TOKEN is required when BROWSERD_CHROME_RELAY_MODE=extension."
205
- );
206
- }
207
+ const chromeRelayExtensionToken =
208
+ resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN) ??
209
+ (chromeRelayMode === "extension" ? DEFAULT_CHROME_RELAY_EXTENSION_TOKEN : undefined);
207
210
 
208
211
  const defaultDriver =
209
- resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ??
210
- (managedLocalEnabled ? MANAGED_LOCAL_DRIVER_KEY : DEFAULT_DRIVER_KEY);
212
+ resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ?? DEFAULT_CONFIG_DRIVER_KEY;
211
213
 
212
214
  return {
213
215
  chromeRelayUrl: env.BROWSERD_CHROME_RELAY_URL ?? "http://127.0.0.1:9223",
@@ -1,9 +1,10 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { createConnection } from "node:net";
4
4
 
5
5
  import { createContainer, loadBrowserdConfig } from "./container";
6
6
  import { bootstrapBrowserd } from "./bootstrap";
7
+ import { reserveLoopbackPort } from "./test-port";
7
8
 
8
9
  function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
9
10
  return new Promise((resolve, reject) => {
@@ -78,11 +79,33 @@ function sendTcpToolRequest(
78
79
  });
79
80
  }
80
81
 
82
+ let testRelayPort = 0;
83
+ let testTcpPort = 0;
84
+
85
+ beforeEach(async () => {
86
+ testRelayPort = await reserveLoopbackPort();
87
+ testTcpPort = await reserveLoopbackPort();
88
+ while (testTcpPort === testRelayPort) {
89
+ testTcpPort = await reserveLoopbackPort();
90
+ }
91
+ });
92
+
93
+ function createTestEnv(
94
+ overrides: Record<string, string | undefined> = {}
95
+ ): Record<string, string | undefined> {
96
+ return {
97
+ BROWSERD_CHROME_RELAY_URL: `http://127.0.0.1:${testRelayPort}`,
98
+ ...overrides
99
+ };
100
+ }
101
+
81
102
  describe("browserd container", () => {
82
103
  it("uses secure defaults for new security config fields", () => {
83
104
  const config = loadBrowserdConfig({});
84
105
 
85
- expect(config.defaultDriver).toBe("managed-local");
106
+ expect(config.defaultDriver).toBe("chrome-relay");
107
+ expect(config.chromeRelayMode).toBe("extension");
108
+ expect(config.chromeRelayExtensionToken).toBe("browserctl-relay");
86
109
  expect(config.managedLocalEnabled).toBe(true);
87
110
  expect(config.uploadRoot).toBeUndefined();
88
111
  expect(config.downloadRoot).toBeUndefined();
@@ -91,12 +114,13 @@ describe("browserd container", () => {
91
114
  });
92
115
 
93
116
  it("registers managed-local, managed, chrome-relay, and remote-cdp drivers by default", () => {
94
- const c = createContainer();
117
+ const c = createContainer(loadBrowserdConfig(createTestEnv()));
95
118
 
96
119
  expect(c.drivers.has("managed-local")).toBe(true);
97
120
  expect(c.drivers.has("managed")).toBe(true);
98
121
  expect(c.drivers.has("chrome-relay")).toBe(true);
99
122
  expect(c.drivers.has("remote-cdp")).toBe(true);
123
+ c.close();
100
124
  });
101
125
 
102
126
  it("uses env override in loadBrowserdConfig", () => {
@@ -110,7 +134,7 @@ describe("browserd container", () => {
110
134
 
111
135
  expect(config.remoteCdpUrl).toBe("http://127.0.0.1:9333/devtools/browser/override");
112
136
  expect(config.chromeRelayUrl).toBe("http://127.0.0.1:9223");
113
- expect(config.defaultDriver).toBe("managed-local");
137
+ expect(config.defaultDriver).toBe("chrome-relay");
114
138
  expect(config.managedLocalEnabled).toBe(true);
115
139
  expect(config.uploadRoot).toBe("C:\\safe\\uploads");
116
140
  expect(config.downloadRoot).toBe("C:\\safe\\downloads");
@@ -132,31 +156,42 @@ describe("browserd container", () => {
132
156
  expect(config.chromeRelayExtensionRequestTimeoutMs).toBe(7000);
133
157
  });
134
158
 
135
- it("requires extension relay token when extension mode is enabled", () => {
136
- expect(() =>
137
- loadBrowserdConfig({
138
- BROWSERD_CHROME_RELAY_MODE: "extension"
139
- })
140
- ).toThrow("BROWSERD_CHROME_RELAY_EXTENSION_TOKEN");
159
+ it("respects explicit cdp relay mode override", () => {
160
+ const config = loadBrowserdConfig({
161
+ BROWSERD_CHROME_RELAY_MODE: "cdp"
162
+ });
163
+
164
+ expect(config.chromeRelayMode).toBe("cdp");
165
+ });
166
+
167
+ it("uses default extension relay token when extension mode is enabled", () => {
168
+ const config = loadBrowserdConfig({
169
+ BROWSERD_CHROME_RELAY_MODE: "extension"
170
+ });
171
+
172
+ expect(config.chromeRelayMode).toBe("extension");
173
+ expect(config.chromeRelayExtensionToken).toBe("browserctl-relay");
141
174
  });
142
175
 
143
- it("defaults to managed when managed-local is explicitly disabled", () => {
176
+ it("keeps chrome-relay as default when managed-local is explicitly disabled", () => {
144
177
  const config = loadBrowserdConfig({
145
178
  BROWSERD_MANAGED_LOCAL_ENABLED: "false"
146
179
  });
147
180
 
148
181
  expect(config.managedLocalEnabled).toBe(false);
149
- expect(config.defaultDriver).toBe("managed");
182
+ expect(config.defaultDriver).toBe("chrome-relay");
150
183
  });
151
184
 
152
185
  it("does not register managed-local driver when explicitly disabled", () => {
153
186
  const c = createContainer(
154
187
  loadBrowserdConfig({
188
+ ...createTestEnv(),
155
189
  BROWSERD_MANAGED_LOCAL_ENABLED: "false"
156
190
  })
157
191
  );
158
192
 
159
193
  expect(c.drivers.has("managed-local")).toBe(false);
194
+ c.close();
160
195
  });
161
196
  });
162
197
 
@@ -165,7 +200,7 @@ describe("browserd bootstrap", () => {
165
200
  const input = new PassThrough();
166
201
  const output = new PassThrough();
167
202
 
168
- const runtime = bootstrapBrowserd({ input, output });
203
+ const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output });
169
204
 
170
205
  expect(runtime.mcpStdioStarted).toBe(true);
171
206
  expect(runtime.container.drivers.has("managed")).toBe(true);
@@ -179,13 +214,13 @@ describe("browserd bootstrap", () => {
179
214
  });
180
215
 
181
216
  it("supports tcp transport mode for persistent daemon use", async () => {
182
- const port = 41419;
217
+ const port = testTcpPort;
183
218
  const runtime = bootstrapBrowserd({
184
- env: {
219
+ env: createTestEnv({
185
220
  BROWSERD_TRANSPORT: "tcp",
186
221
  BROWSERD_PORT: String(port),
187
222
  BROWSERD_AUTH_TOKEN: "tcp-token"
188
- }
223
+ })
189
224
  });
190
225
 
191
226
  const response = await sendTcpToolRequest(port, {
@@ -215,10 +250,10 @@ describe("browserd bootstrap", () => {
215
250
  try {
216
251
  expect(() => {
217
252
  runtime = bootstrapBrowserd({
218
- env: {
253
+ env: createTestEnv({
219
254
  BROWSERD_TRANSPORT: "tcp",
220
- BROWSERD_PORT: "41420"
221
- }
255
+ BROWSERD_PORT: String(testTcpPort)
256
+ })
222
257
  });
223
258
  }).toThrow("BROWSERD_AUTH_TOKEN");
224
259
  } finally {
@@ -229,7 +264,7 @@ describe("browserd bootstrap", () => {
229
264
  it("processes line-delimited requests and writes response with id", async () => {
230
265
  const input = new PassThrough();
231
266
  const output = new PassThrough();
232
- const runtime = bootstrapBrowserd({ input, output, stdioProtocol: "legacy" });
267
+ const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
233
268
 
234
269
  const response = await sendToolRequest(input, output, {
235
270
  id: "request-1",
@@ -247,7 +282,7 @@ describe("browserd bootstrap", () => {
247
282
  expect(response.data).toMatchObject({
248
283
  kind: "browserd",
249
284
  ready: true,
250
- driver: "managed-local"
285
+ driver: "chrome-relay"
251
286
  });
252
287
 
253
288
  runtime.close();
@@ -259,10 +294,10 @@ describe("browserd bootstrap", () => {
259
294
  const input = new PassThrough();
260
295
  const output = new PassThrough();
261
296
  const runtime = bootstrapBrowserd({
262
- env: {
297
+ env: createTestEnv({
263
298
  BROWSERD_MANAGED_LOCAL_ENABLED: "true",
264
299
  BROWSERD_DEFAULT_DRIVER: "managed-local"
265
- },
300
+ }),
266
301
  input,
267
302
  output,
268
303
  stdioProtocol: "legacy"
@@ -297,10 +332,10 @@ describe("browserd bootstrap", () => {
297
332
  const input = new PassThrough();
298
333
  const output = new PassThrough();
299
334
  const runtime = bootstrapBrowserd({
300
- env: {
335
+ env: createTestEnv({
301
336
  BROWSERD_DEFAULT_DRIVER: "managed-local",
302
337
  BROWSERD_MANAGED_LOCAL_ENABLED: "false"
303
- },
338
+ }),
304
339
  input,
305
340
  output,
306
341
  stdioProtocol: "legacy"
@@ -331,9 +366,9 @@ describe("browserd bootstrap", () => {
331
366
  const input = new PassThrough();
332
367
  const output = new PassThrough();
333
368
  const runtime = bootstrapBrowserd({
334
- env: {
369
+ env: createTestEnv({
335
370
  BROWSERD_AUTH_TOKEN: "secret-token"
336
- },
371
+ }),
337
372
  input,
338
373
  output,
339
374
  stdioProtocol: "legacy"
@@ -362,9 +397,9 @@ describe("browserd bootstrap", () => {
362
397
  const input = new PassThrough();
363
398
  const output = new PassThrough();
364
399
  const runtime = bootstrapBrowserd({
365
- env: {
400
+ env: createTestEnv({
366
401
  BROWSERD_AUTH_TOKEN: "secret-token"
367
- },
402
+ }),
368
403
  input,
369
404
  output,
370
405
  stdioProtocol: "legacy"
@@ -394,11 +429,11 @@ describe("browserd bootstrap", () => {
394
429
  const input = new PassThrough();
395
430
  const output = new PassThrough();
396
431
  const runtime = bootstrapBrowserd({
397
- env: {
432
+ env: createTestEnv({
398
433
  BROWSERD_DEFAULT_DRIVER: "managed",
399
434
  BROWSERD_AUTH_TOKEN: "secret-token",
400
435
  BROWSERD_AUTH_SCOPES: "read"
401
- },
436
+ }),
402
437
  input,
403
438
  output,
404
439
  stdioProtocol: "legacy"
@@ -429,11 +464,11 @@ describe("browserd bootstrap", () => {
429
464
  const input = new PassThrough();
430
465
  const output = new PassThrough();
431
466
  const runtime = bootstrapBrowserd({
432
- env: {
467
+ env: createTestEnv({
433
468
  BROWSERD_DEFAULT_DRIVER: "managed",
434
469
  BROWSERD_AUTH_TOKEN: "secret-token",
435
470
  BROWSERD_AUTH_SCOPES: "read,act"
436
- },
471
+ }),
437
472
  input,
438
473
  output,
439
474
  stdioProtocol: "legacy"
@@ -494,10 +529,10 @@ describe("browserd bootstrap", () => {
494
529
  const input = new PassThrough();
495
530
  const output = new PassThrough();
496
531
  const runtime = bootstrapBrowserd({
497
- env: {
532
+ env: createTestEnv({
498
533
  BROWSERD_DEFAULT_DRIVER: "managed",
499
534
  BROWSERD_UPLOAD_ROOT: "C:\\allowed\\uploads"
500
- },
535
+ }),
501
536
  input,
502
537
  output,
503
538
  stdioProtocol: "legacy"
@@ -539,9 +574,9 @@ describe("browserd bootstrap", () => {
539
574
  const input = new PassThrough();
540
575
  const output = new PassThrough();
541
576
  const runtime = bootstrapBrowserd({
542
- env: {
577
+ env: createTestEnv({
543
578
  BROWSERD_DEFAULT_DRIVER: "managed"
544
- },
579
+ }),
545
580
  input,
546
581
  output,
547
582
  stdioProtocol: "legacy"
@@ -584,10 +619,10 @@ describe("browserd bootstrap", () => {
584
619
  const input = new PassThrough();
585
620
  const output = new PassThrough();
586
621
  const runtime = bootstrapBrowserd({
587
- env: {
622
+ env: createTestEnv({
588
623
  BROWSERD_DEFAULT_DRIVER: "managed",
589
624
  BROWSERD_DOWNLOAD_ROOT: "C:\\allowed\\downloads"
590
- },
625
+ }),
591
626
  input,
592
627
  output,
593
628
  stdioProtocol: "legacy"
@@ -646,9 +681,9 @@ describe("browserd bootstrap", () => {
646
681
  const input = new PassThrough();
647
682
  const output = new PassThrough();
648
683
  const runtime = bootstrapBrowserd({
649
- env: {
684
+ env: createTestEnv({
650
685
  BROWSERD_DEFAULT_DRIVER: "managed"
651
- },
686
+ }),
652
687
  input,
653
688
  output,
654
689
  stdioProtocol: "legacy"
@@ -690,9 +725,9 @@ describe("browserd bootstrap", () => {
690
725
  const input = new PassThrough();
691
726
  const output = new PassThrough();
692
727
  const runtime = bootstrapBrowserd({
693
- env: {
728
+ env: createTestEnv({
694
729
  BROWSERD_DEFAULT_DRIVER: "managed"
695
- },
730
+ }),
696
731
  input,
697
732
  output,
698
733
  stdioProtocol: "legacy"
@@ -739,9 +774,9 @@ describe("browserd bootstrap", () => {
739
774
  const input = new PassThrough();
740
775
  const output = new PassThrough();
741
776
  const runtime = bootstrapBrowserd({
742
- env: {
777
+ env: createTestEnv({
743
778
  BROWSERD_DEFAULT_DRIVER: "managed"
744
- },
779
+ }),
745
780
  input,
746
781
  output,
747
782
  stdioProtocol: "legacy"
@@ -819,7 +854,7 @@ describe("browserd bootstrap", () => {
819
854
  it("preserves id and trace/session metadata when queue-level error handling runs", async () => {
820
855
  const input = new PassThrough();
821
856
  const output = new PassThrough();
822
- const runtime = bootstrapBrowserd({ input, output, stdioProtocol: "legacy" });
857
+ const runtime = bootstrapBrowserd({ env: createTestEnv(), input, output, stdioProtocol: "legacy" });
823
858
 
824
859
  const originalWrite = output.write.bind(output);
825
860
  let shouldThrow = true;
@@ -0,0 +1,26 @@
1
+ import { createServer } from "node:net";
2
+
3
+ export async function reserveLoopbackPort(): Promise<number> {
4
+ return await new Promise<number>((resolve, reject) => {
5
+ const server = createServer();
6
+
7
+ server.once("error", reject);
8
+ server.listen(0, "127.0.0.1", () => {
9
+ const address = server.address();
10
+ if (typeof address !== "object" || address === null) {
11
+ server.close(() => reject(new Error("Failed to reserve loopback port.")));
12
+ return;
13
+ }
14
+
15
+ const port = address.port;
16
+ server.close((error) => {
17
+ if (error !== undefined) {
18
+ reject(error);
19
+ return;
20
+ }
21
+
22
+ resolve(port);
23
+ });
24
+ });
25
+ });
26
+ }
@@ -3,6 +3,7 @@ import { PassThrough } from "node:stream";
3
3
  import { describe, expect, it } from "vitest";
4
4
 
5
5
  import { bootstrapBrowserd } from "./bootstrap";
6
+ import { reserveLoopbackPort } from "./test-port";
6
7
 
7
8
  function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
8
9
  return new Promise((resolve, reject) => {
@@ -40,12 +41,15 @@ function sendToolRequest(
40
41
  return responsePromise;
41
42
  }
42
43
 
43
- function createManagedLegacyRuntime() {
44
+ async function createManagedLegacyRuntime() {
45
+ const relayPort = await reserveLoopbackPort();
46
+
44
47
  const input = new PassThrough();
45
48
  const output = new PassThrough();
46
49
  const runtime = bootstrapBrowserd({
47
50
  env: {
48
- BROWSERD_DEFAULT_DRIVER: "managed"
51
+ BROWSERD_DEFAULT_DRIVER: "managed",
52
+ BROWSERD_CHROME_RELAY_URL: `http://127.0.0.1:${relayPort}`
49
53
  },
50
54
  input,
51
55
  output,
@@ -61,7 +65,7 @@ function createManagedLegacyRuntime() {
61
65
 
62
66
  describe("browserd tool matrix", () => {
63
67
  it("returns E_INVALID_ARG for missing required arguments on routed tools", async () => {
64
- const { input, output, runtime } = createManagedLegacyRuntime();
68
+ const { input, output, runtime } = await createManagedLegacyRuntime();
65
69
 
66
70
  try {
67
71
  const toolNames = [
@@ -109,7 +113,7 @@ describe("browserd tool matrix", () => {
109
113
  });
110
114
 
111
115
  it("returns E_DRIVER_UNAVAILABLE for structured action tools on managed driver", async () => {
112
- const { input, output, runtime } = createManagedLegacyRuntime();
116
+ const { input, output, runtime } = await createManagedLegacyRuntime();
113
117
 
114
118
  try {
115
119
  const openResponse = await sendToolRequest(input, output, {
@@ -235,7 +239,7 @@ describe("browserd tool matrix", () => {
235
239
  });
236
240
 
237
241
  it("routes managed-driver tools including profile.use, act, focus/close, and waitFor timeout", async () => {
238
- const { input, output, runtime } = createManagedLegacyRuntime();
242
+ const { input, output, runtime } = await createManagedLegacyRuntime();
239
243
 
240
244
  try {
241
245
  const profileUseResponse = await sendToolRequest(input, output, {
@@ -23,6 +23,7 @@
23
23
  默认 Bridge URL:
24
24
 
25
25
  - `ws://127.0.0.1:9223/bridge`
26
+ - 默认 token(当 daemon 使用默认配置时):`browserctl-relay`
26
27
 
27
28
  ## 弹窗故障排查
28
29
 
@@ -23,6 +23,7 @@ This extension uses the `debugger` permission to collect console/network telemet
23
23
  Default bridge URL:
24
24
 
25
25
  - `ws://127.0.0.1:9223/bridge`
26
+ - Default token (if daemon uses defaults): `browserctl-relay`
26
27
 
27
28
  ## Troubleshooting In Popup
28
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flrande/browserctl",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "browserctl": "bin/browserctl.cjs",
@@ -16,7 +16,8 @@
16
16
  "packages/protocol/src",
17
17
  "packages/transport-mcp-stdio/src",
18
18
  "extensions/chrome-relay",
19
- "scripts/smoke.ps1",
19
+ "INSTALL.md",
20
+ "INSTALL-CN.md",
20
21
  "bin",
21
22
  "README.md",
22
23
  "README-CN.md",
@@ -43,8 +44,9 @@
43
44
  "test:unit": "vitest run --config vitest.config.ts",
44
45
  "test:contract": "vitest run --config vitest.contract.config.ts",
45
46
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
46
- "test:all": "pnpm run test:unit && pnpm run test:contract && pnpm run test:e2e",
47
- "build": "pnpm publish --dry-run --no-git-checks",
47
+ "test:smoke": "vitest run --config vitest.smoke.config.ts",
48
+ "test:all": "pnpm run test:unit && pnpm run test:contract && pnpm run test:e2e && pnpm run test:smoke",
49
+ "build": "npm pack --dry-run",
48
50
  "typecheck": "pnpm exec tsc --noEmit -p tsconfig.typecheck.json",
49
51
  "lint": "node ./scripts/lint.mjs"
50
52
  }
@@ -1,16 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { getSmokeCommand } from "./smoke";
4
-
5
- describe("smoke wiring", () => {
6
- it("returns a PowerShell command with required flags and default script path", () => {
7
- const command = getSmokeCommand();
8
-
9
- expect(command).toContain("pwsh");
10
- expect(command).toContain("-NoLogo");
11
- expect(command).toContain("-NoProfile");
12
- expect(command).toContain("-NonInteractive");
13
- expect(command).toContain("-File");
14
- expect(command).toContain('".\\scripts\\smoke.ps1"');
15
- });
16
- });
@@ -1,5 +0,0 @@
1
- const DEFAULT_SMOKE_SCRIPT_PATH = ".\\scripts\\smoke.ps1";
2
-
3
- export function getSmokeCommand(scriptPath: string = DEFAULT_SMOKE_SCRIPT_PATH): string {
4
- return `pwsh -NoLogo -NoProfile -NonInteractive -File "${scriptPath}"`;
5
- }
package/scripts/smoke.ps1 DELETED
@@ -1,127 +0,0 @@
1
- $ErrorActionPreference = 'Stop'
2
- Set-StrictMode -Version Latest
3
-
4
- $scriptDir = Split-Path -Parent $PSCommandPath
5
- $repoRoot = Resolve-Path -LiteralPath (Join-Path $scriptDir '..')
6
- $browserdEntry = Join-Path $repoRoot 'apps/browserd/src/main.ts'
7
- $browserctlEntry = Join-Path $repoRoot 'apps/browserctl/src/main.ts'
8
-
9
- $daemonProcess = $null
10
- $daemonPort = 41337
11
- $timeoutMs = 10000
12
- $pollIntervalMs = 250
13
- $pnpmExecutable = if ($IsWindows) { 'pnpm.cmd' } else { 'pnpm' }
14
-
15
- function Invoke-SmokeStatus {
16
- param(
17
- [Parameter(Mandatory = $true)]
18
- [string]$BrowserctlEntry
19
- )
20
-
21
- $previousPort = $env:BROWSERCTL_DAEMON_PORT
22
- $env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
23
-
24
- $statusOutput = & pnpm exec tsx $BrowserctlEntry status --json 2>&1
25
- $exitCode = $LASTEXITCODE
26
- $statusText = [string]::Join([Environment]::NewLine, @($statusOutput))
27
-
28
- if ($null -eq $previousPort) {
29
- Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
30
- } else {
31
- $env:BROWSERCTL_DAEMON_PORT = $previousPort
32
- }
33
-
34
- if ($exitCode -ne 0) {
35
- return @{
36
- Ok = $false
37
- Reason = "browserctl status exited with code $exitCode. Output: $statusText"
38
- }
39
- }
40
-
41
- $statusPayload = $null
42
- try {
43
- $statusPayload = $statusText | ConvertFrom-Json -ErrorAction Stop
44
- } catch {
45
- return @{
46
- Ok = $false
47
- Reason = "browserctl status returned invalid JSON. Output: $statusText"
48
- }
49
- }
50
-
51
- if (-not $statusPayload.ok) {
52
- return @{
53
- Ok = $false
54
- Reason = 'browserctl status returned ok=false.'
55
- }
56
- }
57
-
58
- $errorProperty = $statusPayload.PSObject.Properties['error']
59
- if ($null -ne $errorProperty -and $null -ne $errorProperty.Value) {
60
- $errorJson = $errorProperty.Value | ConvertTo-Json -Compress
61
- return @{
62
- Ok = $false
63
- Reason = "browserctl status returned error payload: $errorJson"
64
- }
65
- }
66
-
67
- return @{
68
- Ok = $true
69
- Payload = $statusPayload
70
- }
71
- }
72
-
73
- try {
74
- $previousPort = $env:BROWSERCTL_DAEMON_PORT
75
- $env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
76
- & pnpm exec tsx $browserctlEntry daemon-stop --json *> $null
77
- if ($null -eq $previousPort) {
78
- Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
79
- } else {
80
- $env:BROWSERCTL_DAEMON_PORT = $previousPort
81
- }
82
-
83
- $daemonEnv = @{
84
- BROWSERD_TRANSPORT = 'tcp'
85
- BROWSERD_PORT = "$daemonPort"
86
- }
87
-
88
- $daemonProcess = Start-Process -FilePath $pnpmExecutable `
89
- -ArgumentList @('exec', 'tsx', $browserdEntry) `
90
- -WorkingDirectory $repoRoot `
91
- -Environment $daemonEnv `
92
- -PassThru
93
-
94
- $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
95
- $lastFailure = 'status probe did not run'
96
- $statusResult = $null
97
-
98
- while ($stopwatch.ElapsedMilliseconds -lt $timeoutMs) {
99
- $statusResult = Invoke-SmokeStatus -BrowserctlEntry $browserctlEntry
100
- if ($statusResult.Ok) {
101
- break
102
- }
103
-
104
- $lastFailure = [string]$statusResult.Reason
105
- Start-Sleep -Milliseconds $pollIntervalMs
106
- }
107
-
108
- if ($null -eq $statusResult -or -not $statusResult.Ok) {
109
- $elapsedMs = [int]$stopwatch.ElapsedMilliseconds
110
- throw "Smoke failed: browserctl status --json was not ready within ${timeoutMs}ms (elapsed ${elapsedMs}ms). Last failure: $lastFailure"
111
- }
112
-
113
- Write-Host 'Smoke passed: daemon started and browserctl status --json returned success.'
114
- } finally {
115
- if ($null -ne $daemonProcess -and -not $daemonProcess.HasExited) {
116
- Stop-Process -Id $daemonProcess.Id -ErrorAction SilentlyContinue
117
- }
118
-
119
- $previousPort = $env:BROWSERCTL_DAEMON_PORT
120
- $env:BROWSERCTL_DAEMON_PORT = "$daemonPort"
121
- & pnpm exec tsx $browserctlEntry daemon-stop --json *> $null
122
- if ($null -eq $previousPort) {
123
- Remove-Item Env:\BROWSERCTL_DAEMON_PORT -ErrorAction SilentlyContinue
124
- } else {
125
- $env:BROWSERCTL_DAEMON_PORT = $previousPort
126
- }
127
- }