openclaw-browsy 0.1.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/README.md +91 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +30 -0
- package/dist/context.d.ts +26 -0
- package/dist/context.js +100 -0
- package/dist/core/client.d.ts +70 -0
- package/dist/core/client.js +109 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +4 -0
- package/dist/core/server-manager.d.ts +22 -0
- package/dist/core/server-manager.js +115 -0
- package/dist/core/session-manager.d.ts +20 -0
- package/dist/core/session-manager.js +50 -0
- package/dist/core/tool-definitions.d.ts +6 -0
- package/dist/core/tool-definitions.js +258 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +64 -0
- package/dist/plugin/commands.d.ts +9 -0
- package/dist/plugin/commands.js +38 -0
- package/dist/plugin/gateway.d.ts +10 -0
- package/dist/plugin/gateway.js +15 -0
- package/dist/plugin/hooks.d.ts +12 -0
- package/dist/plugin/hooks.js +36 -0
- package/dist/plugin/index.d.ts +7 -0
- package/dist/plugin/index.js +4 -0
- package/dist/plugin/service.d.ts +10 -0
- package/dist/plugin/service.js +16 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +4 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/process.d.ts +13 -0
- package/dist/utils/process.js +45 -0
- package/package.json +64 -0
- package/skills/browse-and-extract/SKILL.md +83 -0
- package/skills/form-filler/SKILL.md +100 -0
- package/skills/web-research/SKILL.md +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# openclaw-browsy
|
|
2
|
+
|
|
3
|
+
Zero-render browser plugin for OpenClaw. Integrates [browsy](https://github.com/nichochar/agentbrowser) as a fast, lightweight alternative to Playwright/CDP for AI agent browsing tasks.
|
|
4
|
+
|
|
5
|
+
## Why browsy?
|
|
6
|
+
|
|
7
|
+
OpenClaw's built-in browser uses Playwright + CDP: ~300MB RAM, 2-5s per page. browsy handles 70%+ of agent browsing tasks (forms, logins, search, data extraction) at **10x speed** and **60x less memory** by parsing HTML into a Spatial DOM without rendering pixels.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install openclaw-browsy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires the `browsy` CLI binary in your PATH. Install from the [browsy releases](https://github.com/nichochar/agentbrowser/releases).
|
|
16
|
+
|
|
17
|
+
## Configure
|
|
18
|
+
|
|
19
|
+
In your OpenClaw config:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"plugins": {
|
|
24
|
+
"openclaw-browsy": {
|
|
25
|
+
"port": 3847,
|
|
26
|
+
"autoStart": true,
|
|
27
|
+
"allowPrivateNetwork": false,
|
|
28
|
+
"preferBrowsy": true,
|
|
29
|
+
"serverTimeout": 10000
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Default | Description |
|
|
36
|
+
|--------|---------|-------------|
|
|
37
|
+
| `port` | `3847` | Port for the browsy REST server |
|
|
38
|
+
| `autoStart` | `true` | Start `browsy serve` automatically on plugin init |
|
|
39
|
+
| `allowPrivateNetwork` | `false` | Allow fetching private/internal network URLs |
|
|
40
|
+
| `preferBrowsy` | `true` | Intercept built-in browser tool calls and redirect through browsy |
|
|
41
|
+
| `serverTimeout` | `10000` | Timeout (ms) waiting for server startup |
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### As an OpenClaw plugin
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// openclaw.config.ts
|
|
49
|
+
import { register } from "openclaw-browsy";
|
|
50
|
+
export default { register };
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Once registered, every agent gets 13 browsy tools automatically:
|
|
54
|
+
|
|
55
|
+
| Tool | Description |
|
|
56
|
+
|------|-------------|
|
|
57
|
+
| `browsy_browse` | Navigate to a URL |
|
|
58
|
+
| `browsy_click` | Click an element by ID |
|
|
59
|
+
| `browsy_type_text` | Type text into an input |
|
|
60
|
+
| `browsy_check` | Check a checkbox/radio |
|
|
61
|
+
| `browsy_uncheck` | Uncheck a checkbox/radio |
|
|
62
|
+
| `browsy_select` | Select a dropdown option |
|
|
63
|
+
| `browsy_search` | Search the web |
|
|
64
|
+
| `browsy_login` | Log in with credentials |
|
|
65
|
+
| `browsy_enter_code` | Enter a 2FA/verification code |
|
|
66
|
+
| `browsy_find` | Find elements by text or ARIA role |
|
|
67
|
+
| `browsy_page_info` | Get page metadata and suggested actions |
|
|
68
|
+
| `browsy_tables` | Extract structured table data |
|
|
69
|
+
| `browsy_back` | Go back in browsing history |
|
|
70
|
+
|
|
71
|
+
### Standalone
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { BrowsyContext } from "openclaw-browsy";
|
|
75
|
+
|
|
76
|
+
const ctx = new BrowsyContext({ port: 3847, autoStart: false });
|
|
77
|
+
const result = await ctx.executeToolCall("browse", { url: "https://example.com" });
|
|
78
|
+
console.log(result);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Skills
|
|
82
|
+
|
|
83
|
+
Three runtime skills are included:
|
|
84
|
+
|
|
85
|
+
- **browse-and-extract** — Navigate + extract with auto-handling of login walls and cookie consent
|
|
86
|
+
- **web-research** — Search + read multiple pages + compile summary
|
|
87
|
+
- **form-filler** — Detect form fields + fill + submit using page intelligence
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BrowsyConfig, BrowsyConfigInput } from "./types.js";
|
|
2
|
+
export declare function defaultConfig(): BrowsyConfig;
|
|
3
|
+
/**
|
|
4
|
+
* Merge partial user input onto defaults, producing a complete BrowsyConfig.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseConfig(input?: BrowsyConfigInput): BrowsyConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Defaults
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export function defaultConfig() {
|
|
5
|
+
return {
|
|
6
|
+
port: 3847,
|
|
7
|
+
autoStart: true,
|
|
8
|
+
allowPrivateNetwork: false,
|
|
9
|
+
preferBrowsy: true,
|
|
10
|
+
serverTimeout: 10_000,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Config parsing
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Merge partial user input onto defaults, producing a complete BrowsyConfig.
|
|
18
|
+
*/
|
|
19
|
+
export function parseConfig(input) {
|
|
20
|
+
const base = defaultConfig();
|
|
21
|
+
if (!input)
|
|
22
|
+
return base;
|
|
23
|
+
return {
|
|
24
|
+
port: input.port ?? base.port,
|
|
25
|
+
autoStart: input.autoStart ?? base.autoStart,
|
|
26
|
+
allowPrivateNetwork: input.allowPrivateNetwork ?? base.allowPrivateNetwork,
|
|
27
|
+
preferBrowsy: input.preferBrowsy ?? base.preferBrowsy,
|
|
28
|
+
serverTimeout: input.serverTimeout ?? base.serverTimeout,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BrowsyConfig, BrowsyConfigInput, BrowsyServerInfo } from "./types.js";
|
|
2
|
+
import { BrowsyClient } from "./core/client.js";
|
|
3
|
+
import { ServerManager } from "./core/server-manager.js";
|
|
4
|
+
import { SessionManager } from "./core/session-manager.js";
|
|
5
|
+
/**
|
|
6
|
+
* Central facade holding config, client, server manager, and session manager.
|
|
7
|
+
* One instance per plugin lifetime.
|
|
8
|
+
*/
|
|
9
|
+
export declare class BrowsyContext {
|
|
10
|
+
readonly config: BrowsyConfig;
|
|
11
|
+
readonly client: BrowsyClient;
|
|
12
|
+
readonly serverManager: ServerManager;
|
|
13
|
+
readonly sessionManager: SessionManager;
|
|
14
|
+
/** Agent ID to use when no agent context is available */
|
|
15
|
+
private defaultAgentId;
|
|
16
|
+
constructor(configInput?: BrowsyConfigInput);
|
|
17
|
+
/** Ensure the browsy server is running. */
|
|
18
|
+
ensureServer(): Promise<void>;
|
|
19
|
+
/** Get current server status. */
|
|
20
|
+
getStatus(): BrowsyServerInfo;
|
|
21
|
+
/**
|
|
22
|
+
* Execute a browsy tool call.
|
|
23
|
+
* Ensures server is running, manages sessions, calls the client, and returns results.
|
|
24
|
+
*/
|
|
25
|
+
executeToolCall(method: string, params: Record<string, unknown>, agentId?: string): Promise<string>;
|
|
26
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { parseConfig } from "./config.js";
|
|
2
|
+
import { BrowsyClient } from "./core/client.js";
|
|
3
|
+
import { ServerManager } from "./core/server-manager.js";
|
|
4
|
+
import { SessionManager } from "./core/session-manager.js";
|
|
5
|
+
/**
|
|
6
|
+
* Central facade holding config, client, server manager, and session manager.
|
|
7
|
+
* One instance per plugin lifetime.
|
|
8
|
+
*/
|
|
9
|
+
export class BrowsyContext {
|
|
10
|
+
config;
|
|
11
|
+
client;
|
|
12
|
+
serverManager;
|
|
13
|
+
sessionManager;
|
|
14
|
+
/** Agent ID to use when no agent context is available */
|
|
15
|
+
defaultAgentId = "__default__";
|
|
16
|
+
constructor(configInput) {
|
|
17
|
+
this.config = parseConfig(configInput);
|
|
18
|
+
this.client = new BrowsyClient(this.config.port);
|
|
19
|
+
this.serverManager = new ServerManager(this.config);
|
|
20
|
+
this.sessionManager = new SessionManager();
|
|
21
|
+
}
|
|
22
|
+
/** Ensure the browsy server is running. */
|
|
23
|
+
async ensureServer() {
|
|
24
|
+
if (!this.serverManager.isRunning()) {
|
|
25
|
+
await this.serverManager.start();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Get current server status. */
|
|
29
|
+
getStatus() {
|
|
30
|
+
return this.serverManager.getStatus();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Execute a browsy tool call.
|
|
34
|
+
* Ensures server is running, manages sessions, calls the client, and returns results.
|
|
35
|
+
*/
|
|
36
|
+
async executeToolCall(method, params, agentId) {
|
|
37
|
+
await this.ensureServer();
|
|
38
|
+
const aid = agentId ?? this.defaultAgentId;
|
|
39
|
+
const session = this.sessionManager.getOrCreate(aid);
|
|
40
|
+
const token = session.token || undefined;
|
|
41
|
+
let response;
|
|
42
|
+
switch (method) {
|
|
43
|
+
case "browse":
|
|
44
|
+
response = await this.client.browse(params, token);
|
|
45
|
+
break;
|
|
46
|
+
case "click":
|
|
47
|
+
response = await this.client.click(params, token);
|
|
48
|
+
break;
|
|
49
|
+
case "typeText":
|
|
50
|
+
response = await this.client.typeText(params, token);
|
|
51
|
+
break;
|
|
52
|
+
case "check":
|
|
53
|
+
response = await this.client.check(params, token);
|
|
54
|
+
break;
|
|
55
|
+
case "uncheck":
|
|
56
|
+
response = await this.client.uncheck(params, token);
|
|
57
|
+
break;
|
|
58
|
+
case "select":
|
|
59
|
+
response = await this.client.select(params, token);
|
|
60
|
+
break;
|
|
61
|
+
case "search":
|
|
62
|
+
response = await this.client.search(params, token);
|
|
63
|
+
break;
|
|
64
|
+
case "login":
|
|
65
|
+
response = await this.client.login(params, token);
|
|
66
|
+
break;
|
|
67
|
+
case "enterCode":
|
|
68
|
+
response = await this.client.enterCode(params, token);
|
|
69
|
+
break;
|
|
70
|
+
case "find":
|
|
71
|
+
response = await this.client.find(params, token);
|
|
72
|
+
break;
|
|
73
|
+
case "pageInfo":
|
|
74
|
+
response = await this.client.pageInfo(token);
|
|
75
|
+
break;
|
|
76
|
+
case "tables":
|
|
77
|
+
response = await this.client.tables(token);
|
|
78
|
+
break;
|
|
79
|
+
case "getPage":
|
|
80
|
+
response = await this.client.getPage(params, token);
|
|
81
|
+
break;
|
|
82
|
+
case "back":
|
|
83
|
+
response = await this.client.back(token);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unknown browsy method: ${method}`);
|
|
87
|
+
}
|
|
88
|
+
// Update session token from response
|
|
89
|
+
if (response.session) {
|
|
90
|
+
this.sessionManager.update(aid, response.session);
|
|
91
|
+
}
|
|
92
|
+
// Append CAPTCHA/blocked fallback guidance if detected
|
|
93
|
+
let result = response.body;
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const errorJson = response.json;
|
|
96
|
+
result = errorJson?.error ?? response.body;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BrowsyResponse } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP client for the browsy REST API.
|
|
4
|
+
* Uses Node.js built-in fetch(). Passes and extracts X-Browsy-Session header.
|
|
5
|
+
*/
|
|
6
|
+
export declare class BrowsyClient {
|
|
7
|
+
private baseUrl;
|
|
8
|
+
constructor(port: number);
|
|
9
|
+
/** GET /health */
|
|
10
|
+
health(): Promise<BrowsyResponse>;
|
|
11
|
+
/** POST /api/browse */
|
|
12
|
+
browse(params: {
|
|
13
|
+
url: string;
|
|
14
|
+
format?: string;
|
|
15
|
+
scope?: string;
|
|
16
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
17
|
+
/** POST /api/click */
|
|
18
|
+
click(params: {
|
|
19
|
+
id: number;
|
|
20
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
21
|
+
/** POST /api/type */
|
|
22
|
+
typeText(params: {
|
|
23
|
+
id: number;
|
|
24
|
+
text: string;
|
|
25
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
26
|
+
/** POST /api/check */
|
|
27
|
+
check(params: {
|
|
28
|
+
id: number;
|
|
29
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
30
|
+
/** POST /api/uncheck */
|
|
31
|
+
uncheck(params: {
|
|
32
|
+
id: number;
|
|
33
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
34
|
+
/** POST /api/select */
|
|
35
|
+
select(params: {
|
|
36
|
+
id: number;
|
|
37
|
+
value: string;
|
|
38
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
39
|
+
/** POST /api/search */
|
|
40
|
+
search(params: {
|
|
41
|
+
query: string;
|
|
42
|
+
engine?: string;
|
|
43
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
44
|
+
/** POST /api/login */
|
|
45
|
+
login(params: {
|
|
46
|
+
username: string;
|
|
47
|
+
password: string;
|
|
48
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
49
|
+
/** POST /api/enter-code */
|
|
50
|
+
enterCode(params: {
|
|
51
|
+
code: string;
|
|
52
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
53
|
+
/** POST /api/find */
|
|
54
|
+
find(params: {
|
|
55
|
+
text?: string;
|
|
56
|
+
role?: string;
|
|
57
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
58
|
+
/** GET /api/page */
|
|
59
|
+
getPage(params?: {
|
|
60
|
+
format?: string;
|
|
61
|
+
scope?: string;
|
|
62
|
+
}, session?: string): Promise<BrowsyResponse>;
|
|
63
|
+
/** GET /api/page-info */
|
|
64
|
+
pageInfo(session?: string): Promise<BrowsyResponse>;
|
|
65
|
+
/** GET /api/tables */
|
|
66
|
+
tables(session?: string): Promise<BrowsyResponse>;
|
|
67
|
+
/** POST /api/back */
|
|
68
|
+
back(session?: string): Promise<BrowsyResponse>;
|
|
69
|
+
private request;
|
|
70
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the browsy REST API.
|
|
3
|
+
* Uses Node.js built-in fetch(). Passes and extracts X-Browsy-Session header.
|
|
4
|
+
*/
|
|
5
|
+
export class BrowsyClient {
|
|
6
|
+
baseUrl;
|
|
7
|
+
constructor(port) {
|
|
8
|
+
this.baseUrl = `http://127.0.0.1:${port}`;
|
|
9
|
+
}
|
|
10
|
+
/** GET /health */
|
|
11
|
+
async health() {
|
|
12
|
+
return this.request("GET", "/health");
|
|
13
|
+
}
|
|
14
|
+
/** POST /api/browse */
|
|
15
|
+
async browse(params, session) {
|
|
16
|
+
return this.request("POST", "/api/browse", params, session);
|
|
17
|
+
}
|
|
18
|
+
/** POST /api/click */
|
|
19
|
+
async click(params, session) {
|
|
20
|
+
return this.request("POST", "/api/click", params, session);
|
|
21
|
+
}
|
|
22
|
+
/** POST /api/type */
|
|
23
|
+
async typeText(params, session) {
|
|
24
|
+
return this.request("POST", "/api/type", params, session);
|
|
25
|
+
}
|
|
26
|
+
/** POST /api/check */
|
|
27
|
+
async check(params, session) {
|
|
28
|
+
return this.request("POST", "/api/check", params, session);
|
|
29
|
+
}
|
|
30
|
+
/** POST /api/uncheck */
|
|
31
|
+
async uncheck(params, session) {
|
|
32
|
+
return this.request("POST", "/api/uncheck", params, session);
|
|
33
|
+
}
|
|
34
|
+
/** POST /api/select */
|
|
35
|
+
async select(params, session) {
|
|
36
|
+
return this.request("POST", "/api/select", params, session);
|
|
37
|
+
}
|
|
38
|
+
/** POST /api/search */
|
|
39
|
+
async search(params, session) {
|
|
40
|
+
return this.request("POST", "/api/search", params, session);
|
|
41
|
+
}
|
|
42
|
+
/** POST /api/login */
|
|
43
|
+
async login(params, session) {
|
|
44
|
+
return this.request("POST", "/api/login", params, session);
|
|
45
|
+
}
|
|
46
|
+
/** POST /api/enter-code */
|
|
47
|
+
async enterCode(params, session) {
|
|
48
|
+
return this.request("POST", "/api/enter-code", params, session);
|
|
49
|
+
}
|
|
50
|
+
/** POST /api/find */
|
|
51
|
+
async find(params, session) {
|
|
52
|
+
return this.request("POST", "/api/find", params, session);
|
|
53
|
+
}
|
|
54
|
+
/** GET /api/page */
|
|
55
|
+
async getPage(params, session) {
|
|
56
|
+
const query = new URLSearchParams();
|
|
57
|
+
if (params?.format)
|
|
58
|
+
query.set("format", params.format);
|
|
59
|
+
if (params?.scope)
|
|
60
|
+
query.set("scope", params.scope);
|
|
61
|
+
const qs = query.toString();
|
|
62
|
+
return this.request("GET", `/api/page${qs ? `?${qs}` : ""}`, undefined, session);
|
|
63
|
+
}
|
|
64
|
+
/** GET /api/page-info */
|
|
65
|
+
async pageInfo(session) {
|
|
66
|
+
return this.request("GET", "/api/page-info", undefined, session);
|
|
67
|
+
}
|
|
68
|
+
/** GET /api/tables */
|
|
69
|
+
async tables(session) {
|
|
70
|
+
return this.request("GET", "/api/tables", undefined, session);
|
|
71
|
+
}
|
|
72
|
+
/** POST /api/back */
|
|
73
|
+
async back(session) {
|
|
74
|
+
return this.request("POST", "/api/back", undefined, session);
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Internal
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
async request(method, path, body, session) {
|
|
80
|
+
const headers = {};
|
|
81
|
+
if (session) {
|
|
82
|
+
headers["X-Browsy-Session"] = session;
|
|
83
|
+
}
|
|
84
|
+
if (body !== undefined) {
|
|
85
|
+
headers["Content-Type"] = "application/json";
|
|
86
|
+
}
|
|
87
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
88
|
+
method,
|
|
89
|
+
headers,
|
|
90
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
91
|
+
});
|
|
92
|
+
const responseSession = res.headers.get("X-Browsy-Session") ?? session ?? "";
|
|
93
|
+
const text = await res.text();
|
|
94
|
+
let json;
|
|
95
|
+
try {
|
|
96
|
+
json = JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Response is plain text, not JSON
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
ok: res.ok,
|
|
103
|
+
status: res.status,
|
|
104
|
+
session: responseSession,
|
|
105
|
+
body: text,
|
|
106
|
+
json,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BrowsyConfig, BrowsyServerInfo } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Manages the lifecycle of a `browsy serve` child process.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ServerManager {
|
|
6
|
+
private process;
|
|
7
|
+
private status;
|
|
8
|
+
private error;
|
|
9
|
+
private config;
|
|
10
|
+
private client;
|
|
11
|
+
constructor(config: BrowsyConfig);
|
|
12
|
+
/** Start the browsy server. No-op if already running. */
|
|
13
|
+
start(): Promise<void>;
|
|
14
|
+
/** Stop the browsy server. */
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
/** Whether the server is running. */
|
|
17
|
+
isRunning(): boolean;
|
|
18
|
+
/** Get the current server status. */
|
|
19
|
+
getStatus(): BrowsyServerInfo;
|
|
20
|
+
/** Poll /health until ready or timeout. */
|
|
21
|
+
waitForReady(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { BrowsyClient } from "./client.js";
|
|
3
|
+
import { findBrowsyBinary, isPortInUse } from "../utils/process.js";
|
|
4
|
+
/**
|
|
5
|
+
* Manages the lifecycle of a `browsy serve` child process.
|
|
6
|
+
*/
|
|
7
|
+
export class ServerManager {
|
|
8
|
+
process = null;
|
|
9
|
+
status = "stopped";
|
|
10
|
+
error;
|
|
11
|
+
config;
|
|
12
|
+
client;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.client = new BrowsyClient(config.port);
|
|
16
|
+
}
|
|
17
|
+
/** Start the browsy server. No-op if already running. */
|
|
18
|
+
async start() {
|
|
19
|
+
if (this.status === "running")
|
|
20
|
+
return;
|
|
21
|
+
// Check if something is already listening on the port
|
|
22
|
+
if (await isPortInUse(this.config.port)) {
|
|
23
|
+
// Verify it's actually browsy by hitting /health
|
|
24
|
+
try {
|
|
25
|
+
const res = await this.client.health();
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
this.status = "running";
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Port in use but not browsy — error
|
|
33
|
+
}
|
|
34
|
+
this.status = "error";
|
|
35
|
+
this.error = `Port ${this.config.port} is already in use by another process`;
|
|
36
|
+
throw new Error(this.error);
|
|
37
|
+
}
|
|
38
|
+
const binary = findBrowsyBinary();
|
|
39
|
+
if (!binary) {
|
|
40
|
+
this.status = "error";
|
|
41
|
+
this.error = "browsy binary not found in PATH";
|
|
42
|
+
throw new Error(this.error);
|
|
43
|
+
}
|
|
44
|
+
this.status = "starting";
|
|
45
|
+
this.error = undefined;
|
|
46
|
+
const args = ["serve", "--port", String(this.config.port)];
|
|
47
|
+
if (this.config.allowPrivateNetwork) {
|
|
48
|
+
args.push("--allow-private-network");
|
|
49
|
+
}
|
|
50
|
+
this.process = spawn(binary, args, {
|
|
51
|
+
stdio: "pipe",
|
|
52
|
+
detached: false,
|
|
53
|
+
});
|
|
54
|
+
this.process.on("error", (err) => {
|
|
55
|
+
this.status = "error";
|
|
56
|
+
this.error = err.message;
|
|
57
|
+
});
|
|
58
|
+
this.process.on("exit", (code) => {
|
|
59
|
+
if (this.status !== "error") {
|
|
60
|
+
this.status = "stopped";
|
|
61
|
+
}
|
|
62
|
+
if (code !== null && code !== 0) {
|
|
63
|
+
this.status = "error";
|
|
64
|
+
this.error = `browsy exited with code ${code}`;
|
|
65
|
+
}
|
|
66
|
+
this.process = null;
|
|
67
|
+
});
|
|
68
|
+
await this.waitForReady();
|
|
69
|
+
}
|
|
70
|
+
/** Stop the browsy server. */
|
|
71
|
+
async stop() {
|
|
72
|
+
if (!this.process) {
|
|
73
|
+
this.status = "stopped";
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.process.kill();
|
|
77
|
+
this.process = null;
|
|
78
|
+
this.status = "stopped";
|
|
79
|
+
this.error = undefined;
|
|
80
|
+
}
|
|
81
|
+
/** Whether the server is running. */
|
|
82
|
+
isRunning() {
|
|
83
|
+
return this.status === "running";
|
|
84
|
+
}
|
|
85
|
+
/** Get the current server status. */
|
|
86
|
+
getStatus() {
|
|
87
|
+
return {
|
|
88
|
+
status: this.status,
|
|
89
|
+
port: this.config.port,
|
|
90
|
+
pid: this.process?.pid,
|
|
91
|
+
error: this.error,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Poll /health until ready or timeout. */
|
|
95
|
+
async waitForReady() {
|
|
96
|
+
const deadline = Date.now() + this.config.serverTimeout;
|
|
97
|
+
const interval = 200;
|
|
98
|
+
while (Date.now() < deadline) {
|
|
99
|
+
try {
|
|
100
|
+
const res = await this.client.health();
|
|
101
|
+
if (res.ok) {
|
|
102
|
+
this.status = "running";
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Server not ready yet
|
|
108
|
+
}
|
|
109
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
110
|
+
}
|
|
111
|
+
this.status = "error";
|
|
112
|
+
this.error = `browsy server did not become ready within ${this.config.serverTimeout}ms`;
|
|
113
|
+
throw new Error(this.error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BrowsySession } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Maps agentId -> browsy session token.
|
|
4
|
+
* Each agent gets its own isolated browsing session.
|
|
5
|
+
*/
|
|
6
|
+
export declare class SessionManager {
|
|
7
|
+
private sessions;
|
|
8
|
+
/** Get existing session or create a placeholder for the agent. */
|
|
9
|
+
getOrCreate(agentId: string): BrowsySession;
|
|
10
|
+
/** Update the session token (from X-Browsy-Session response header). */
|
|
11
|
+
update(agentId: string, token: string): void;
|
|
12
|
+
/** Remove an agent's session. */
|
|
13
|
+
remove(agentId: string): boolean;
|
|
14
|
+
/** Get a session by agentId. */
|
|
15
|
+
get(agentId: string): BrowsySession | undefined;
|
|
16
|
+
/** List all active sessions. */
|
|
17
|
+
listSessions(): BrowsySession[];
|
|
18
|
+
/** Number of active sessions. */
|
|
19
|
+
count(): number;
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps agentId -> browsy session token.
|
|
3
|
+
* Each agent gets its own isolated browsing session.
|
|
4
|
+
*/
|
|
5
|
+
export class SessionManager {
|
|
6
|
+
sessions = new Map();
|
|
7
|
+
/** Get existing session or create a placeholder for the agent. */
|
|
8
|
+
getOrCreate(agentId) {
|
|
9
|
+
const existing = this.sessions.get(agentId);
|
|
10
|
+
if (existing)
|
|
11
|
+
return existing;
|
|
12
|
+
const session = {
|
|
13
|
+
agentId,
|
|
14
|
+
token: "", // Will be populated from server response
|
|
15
|
+
createdAt: new Date().toISOString(),
|
|
16
|
+
};
|
|
17
|
+
this.sessions.set(agentId, session);
|
|
18
|
+
return session;
|
|
19
|
+
}
|
|
20
|
+
/** Update the session token (from X-Browsy-Session response header). */
|
|
21
|
+
update(agentId, token) {
|
|
22
|
+
const session = this.sessions.get(agentId);
|
|
23
|
+
if (session) {
|
|
24
|
+
session.token = token;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
this.sessions.set(agentId, {
|
|
28
|
+
agentId,
|
|
29
|
+
token,
|
|
30
|
+
createdAt: new Date().toISOString(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Remove an agent's session. */
|
|
35
|
+
remove(agentId) {
|
|
36
|
+
return this.sessions.delete(agentId);
|
|
37
|
+
}
|
|
38
|
+
/** Get a session by agentId. */
|
|
39
|
+
get(agentId) {
|
|
40
|
+
return this.sessions.get(agentId);
|
|
41
|
+
}
|
|
42
|
+
/** List all active sessions. */
|
|
43
|
+
listSessions() {
|
|
44
|
+
return Array.from(this.sessions.values());
|
|
45
|
+
}
|
|
46
|
+
/** Number of active sessions. */
|
|
47
|
+
count() {
|
|
48
|
+
return this.sessions.size;
|
|
49
|
+
}
|
|
50
|
+
}
|