@kritchoff/agent-browser 1.0.3 → 1.0.5
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 +8 -1
- package/dist/cli.cjs +654 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +627 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.cjs +5722 -0
- package/dist/daemon.cjs.map +1 -0
- package/dist/daemon.d.cts +60 -0
- package/dist/daemon.d.ts +14 -13
- package/dist/daemon.js +5659 -387
- package/dist/daemon.js.map +1 -1
- package/dist/index.cjs +630 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -0
- package/dist/index.d.ts +82 -10
- package/dist/index.js +577 -234
- package/dist/index.js.map +1 -1
- package/docker-compose.sdk.yml +2 -1
- package/package.json +30 -10
- package/bin/agent-browser.js +0 -70
- package/dist/actions.d.ts +0 -17
- package/dist/actions.d.ts.map +0 -1
- package/dist/actions.js +0 -1452
- package/dist/actions.js.map +0 -1
- package/dist/browser.d.ts +0 -478
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js +0 -1577
- package/dist/browser.js.map +0 -1
- package/dist/cdp-client.d.ts +0 -103
- package/dist/cdp-client.d.ts.map +0 -1
- package/dist/cdp-client.js +0 -223
- package/dist/cdp-client.js.map +0 -1
- package/dist/daemon.d.ts.map +0 -1
- package/dist/dualmode-config.d.ts +0 -37
- package/dist/dualmode-config.d.ts.map +0 -1
- package/dist/dualmode-config.js +0 -44
- package/dist/dualmode-config.js.map +0 -1
- package/dist/dualmode-fetcher.d.ts +0 -60
- package/dist/dualmode-fetcher.d.ts.map +0 -1
- package/dist/dualmode-fetcher.js +0 -449
- package/dist/dualmode-fetcher.js.map +0 -1
- package/dist/dualmode-types.d.ts +0 -183
- package/dist/dualmode-types.d.ts.map +0 -1
- package/dist/dualmode-types.js +0 -8
- package/dist/dualmode-types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/ios-actions.d.ts +0 -11
- package/dist/ios-actions.d.ts.map +0 -1
- package/dist/ios-actions.js +0 -228
- package/dist/ios-actions.js.map +0 -1
- package/dist/ios-manager.d.ts +0 -266
- package/dist/ios-manager.d.ts.map +0 -1
- package/dist/ios-manager.js +0 -1073
- package/dist/ios-manager.js.map +0 -1
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.d.ts.map +0 -1
- package/dist/orchestrator.js +0 -257
- package/dist/orchestrator.js.map +0 -1
- package/dist/protocol.d.ts +0 -26
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -832
- package/dist/protocol.js.map +0 -1
- package/dist/snapshot.d.ts +0 -76
- package/dist/snapshot.d.ts.map +0 -1
- package/dist/snapshot.js +0 -492
- package/dist/snapshot.js.map +0 -1
- package/dist/stream-server.d.ts +0 -117
- package/dist/stream-server.d.ts.map +0 -1
- package/dist/stream-server.js +0 -305
- package/dist/stream-server.js.map +0 -1
- package/dist/taxtree.d.ts +0 -6
- package/dist/taxtree.d.ts.map +0 -1
- package/dist/taxtree.js +0 -287
- package/dist/taxtree.js.map +0 -1
- package/dist/types.d.ts +0 -742
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/scripts/check-version-sync.js +0 -39
- package/scripts/sync-version.js +0 -69
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
<strong>The Ultimate Headless Android Browser SDK & CLI for AI Agents</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@kritchoff/agent-browser"><img src="https://img.shields.io/npm/v/@kritchoff/agent-browser.svg" alt="NPM Version" /></a>
|
|
9
|
+
<a href="https://github.com/kritagya-khanna/agent-browser/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/kritagya-khanna/agent-browser/ci.yml?branch=main" alt="Build Status" /></a>
|
|
10
|
+
<a href="https://github.com/kritagya-khanna/agent-browser/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@kritchoff/agent-browser.svg" alt="License" /></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/Coverage-90%25-brightgreen.svg" alt="Coverage" />
|
|
12
|
+
<img src="https://img.shields.io/badge/TypeScript-Strict-blue.svg" alt="TypeScript Strict" />
|
|
13
|
+
</p>
|
|
14
|
+
|
|
7
15
|
This package provides a **Real Android Browser** (WootzApp) wrapped in a Docker container, controlled by a high-speed, Playwright-like TypeScript daemon.
|
|
8
16
|
|
|
9
17
|
It is specifically designed for AI Agents to navigate the mobile web, bypass bot detection, and generate LLM-friendly semantic trees (`AXTree`).
|
|
@@ -12,7 +20,6 @@ It is specifically designed for AI Agents to navigate the mobile web, bypass bot
|
|
|
12
20
|
|
|
13
21
|
## 🌟 Key Features
|
|
14
22
|
|
|
15
|
-
- **Cross-Platform**: Works natively on **Windows, macOS (Intel & Apple Silicon), and Linux** using a pure Node.js Orchestrator (No WSL or bash required).
|
|
16
23
|
- **Bundled CLI**: Control the browser directly from your terminal (`agent-browser start`).
|
|
17
24
|
- **Zero-Config Setup**: Automatically downloads and orchestrates the required Docker containers.
|
|
18
25
|
- **Hyper-Speed Warm Boots**: Uses advanced VDI Volume Mounting to boot the Android environment in **< 5 seconds** after the first run.
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
27
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
28
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var net = __toESM(require("net"), 1);
|
|
32
|
+
var import_crypto = require("crypto");
|
|
33
|
+
|
|
34
|
+
// src/orchestrator.ts
|
|
35
|
+
var import_child_process = require("child_process");
|
|
36
|
+
var import_path = __toESM(require("path"), 1);
|
|
37
|
+
var import_url = require("url");
|
|
38
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
39
|
+
var __dirname = import_path.default.dirname((0, import_url.fileURLToPath)(importMetaUrl));
|
|
40
|
+
var PROJECT_ROOT = import_path.default.resolve(__dirname, "..");
|
|
41
|
+
var Orchestrator = class {
|
|
42
|
+
composeFile;
|
|
43
|
+
logger;
|
|
44
|
+
constructor(distMode, logger) {
|
|
45
|
+
this.composeFile = import_path.default.join(
|
|
46
|
+
PROJECT_ROOT,
|
|
47
|
+
distMode ? "docker-compose.sdk.yml" : "docker-compose.prod.yml"
|
|
48
|
+
);
|
|
49
|
+
this.logger = logger || (0, import_pino.default)();
|
|
50
|
+
}
|
|
51
|
+
log(msg, level = "info") {
|
|
52
|
+
switch (level) {
|
|
53
|
+
case "success":
|
|
54
|
+
this.logger.info({ sdk: true }, msg);
|
|
55
|
+
break;
|
|
56
|
+
case "warn":
|
|
57
|
+
this.logger.warn({ sdk: true }, msg);
|
|
58
|
+
break;
|
|
59
|
+
case "error":
|
|
60
|
+
this.logger.error({ sdk: true }, msg);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
this.logger.info({ sdk: true }, msg);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
runCommand(cmd, env = {}) {
|
|
67
|
+
try {
|
|
68
|
+
(0, import_child_process.execSync)(cmd, {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: { ...process.env, ...env }
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
getContainerId(serviceName) {
|
|
78
|
+
try {
|
|
79
|
+
const output = (0, import_child_process.execSync)(`docker compose -f "${this.composeFile}" ps -q ${serviceName}`, {
|
|
80
|
+
encoding: "utf-8"
|
|
81
|
+
});
|
|
82
|
+
return output.trim() || null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
isPortMapped(containerId, port) {
|
|
88
|
+
try {
|
|
89
|
+
const output = (0, import_child_process.execSync)(`docker port "${containerId}" ${port}`, { encoding: "utf-8" });
|
|
90
|
+
return output.includes("0.0.0.0:") || output.includes(":::");
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async pullImagesWithRetry() {
|
|
96
|
+
if (!this.composeFile.endsWith("docker-compose.sdk.yml")) return;
|
|
97
|
+
this.log("Please wait while we get things ready...", "info");
|
|
98
|
+
const maxRetries = 10;
|
|
99
|
+
for (let count = 0; count < maxRetries; count++) {
|
|
100
|
+
if (this.runCommand(`docker compose -f "${this.composeFile}" pull`)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.log(
|
|
104
|
+
`Download failed/interrupted. Retrying (${count + 1}/${maxRetries}) in 10s...`,
|
|
105
|
+
"warn"
|
|
106
|
+
);
|
|
107
|
+
await new Promise((r) => setTimeout(r, 1e4));
|
|
108
|
+
}
|
|
109
|
+
this.log(
|
|
110
|
+
`Failed to download images after ${maxRetries} attempts. Check internet connection.`,
|
|
111
|
+
"error"
|
|
112
|
+
);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
async hasSnapshot() {
|
|
116
|
+
try {
|
|
117
|
+
(0, import_child_process.execSync)(`docker volume inspect agent-snapshots`, { stdio: "ignore" });
|
|
118
|
+
const output = (0, import_child_process.execSync)(`docker run --rm -v agent-snapshots:/data busybox ls /data`, {
|
|
119
|
+
encoding: "utf-8"
|
|
120
|
+
});
|
|
121
|
+
return output.includes("quickboot");
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async start() {
|
|
127
|
+
this.log(`Initializing Agent Environment...`, "info");
|
|
128
|
+
await this.pullImagesWithRetry();
|
|
129
|
+
this.log("Cleaning up previous container state...", "info");
|
|
130
|
+
this.runCommand(`docker compose -f "${this.composeFile}" down -v --remove-orphans`);
|
|
131
|
+
const hasSnapshot = await this.hasSnapshot();
|
|
132
|
+
if (hasSnapshot) {
|
|
133
|
+
this.log("Found cached baseline snapshot. Performing HYPER-SPEED WARM BOOT...", "success");
|
|
134
|
+
const success = this.runCommand(`docker compose -f "${this.composeFile}" up -d`, {
|
|
135
|
+
EMULATOR_SNAPSHOT_NAME: "quickboot"
|
|
136
|
+
});
|
|
137
|
+
if (!success) throw new Error("Startup failed during docker compose up.");
|
|
138
|
+
const agentCont = this.getContainerId("agent-service");
|
|
139
|
+
const androidCont = this.getContainerId("android-service");
|
|
140
|
+
if (agentCont && !this.isPortMapped(agentCont, 3e3)) {
|
|
141
|
+
this.log("Port 3000 (Host 32001) is not mapped! Container config is stale.", "warn");
|
|
142
|
+
this.log("Forcing full restart to apply network settings...", "info");
|
|
143
|
+
this.runCommand(`docker compose -f "${this.composeFile}" down -v --remove-orphans`);
|
|
144
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
145
|
+
this.runCommand(`docker compose -f "${this.composeFile}" up -d`, {
|
|
146
|
+
EMULATOR_SNAPSHOT_NAME: "quickboot"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (androidCont) {
|
|
150
|
+
this.log("Rehydrating Android network connection...", "info");
|
|
151
|
+
try {
|
|
152
|
+
(0, import_child_process.execSync)(`docker exec "${androidCont}" adb shell cmd connectivity airplane-mode enable`, {
|
|
153
|
+
stdio: "ignore"
|
|
154
|
+
});
|
|
155
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
156
|
+
(0, import_child_process.execSync)(
|
|
157
|
+
`docker exec "${androidCont}" adb shell cmd connectivity airplane-mode disable`,
|
|
158
|
+
{ stdio: "ignore" }
|
|
159
|
+
);
|
|
160
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
161
|
+
} catch (e) {
|
|
162
|
+
this.log(`Network rehydration warning: ${e}`, "warn");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.log("Waiting for Daemon Connection...");
|
|
166
|
+
if (agentCont) await this.waitForLog(agentCont, "Daemon listening on TCP", 18e4);
|
|
167
|
+
} else {
|
|
168
|
+
this.log("No baseline found. Performing FIRST RUN SETUP (Cold Boot)...", "warn");
|
|
169
|
+
this.log("This will take ~60-90 seconds, but only once.", "warn");
|
|
170
|
+
const success = this.runCommand(`docker compose -f "${this.composeFile}" up -d`, {
|
|
171
|
+
EMULATOR_SNAPSHOT_NAME: ""
|
|
172
|
+
});
|
|
173
|
+
if (!success) throw new Error("Startup failed during docker compose up.");
|
|
174
|
+
let androidCont = this.getContainerId("android-service");
|
|
175
|
+
const agentCont = this.getContainerId("agent-service");
|
|
176
|
+
if (!androidCont) throw new Error("Error: Android container not found.");
|
|
177
|
+
if (agentCont && !this.isPortMapped(agentCont, 3e3)) {
|
|
178
|
+
this.log("Port 3000 (Host 32001) is not mapped! Container config is stale.", "warn");
|
|
179
|
+
this.log("Forcing full restart to apply network settings...", "info");
|
|
180
|
+
this.runCommand(`docker compose -f "${this.composeFile}" down -v --remove-orphans`);
|
|
181
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
182
|
+
this.runCommand(`docker compose -f "${this.composeFile}" up -d`, {
|
|
183
|
+
EMULATOR_SNAPSHOT_NAME: ""
|
|
184
|
+
});
|
|
185
|
+
androidCont = this.getContainerId("android-service") || androidCont;
|
|
186
|
+
}
|
|
187
|
+
this.log("Waiting for Android OS to boot (this takes a moment)...", "info");
|
|
188
|
+
await this.waitForLog(androidCont, "Emulator boot complete");
|
|
189
|
+
this.log("Waiting for Browser Installation...", "info");
|
|
190
|
+
await this.waitForLog(androidCont, "APK installation complete");
|
|
191
|
+
this.log("Waiting for CDP Bridge...", "info");
|
|
192
|
+
await this.waitForLog(androidCont, "CDP Bridge ready");
|
|
193
|
+
this.log("Waiting for Agent Daemon Connection...");
|
|
194
|
+
if (agentCont) await this.waitForLog(agentCont, "Daemon listening on TCP");
|
|
195
|
+
this.log("Saving emulator state (quickboot)...", "info");
|
|
196
|
+
const saved = this.runCommand(
|
|
197
|
+
`docker exec "${androidCont}" adb emu avd snapshot save quickboot`
|
|
198
|
+
);
|
|
199
|
+
if (saved) {
|
|
200
|
+
this.log("Snapshot saved to host volume.", "success");
|
|
201
|
+
this.log("Setup Complete! Future runs will launch instantly.", "success");
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error("Failed to save snapshot inside emulator.");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async stop() {
|
|
208
|
+
this.log("Stopping environment...", "info");
|
|
209
|
+
this.runCommand(`docker compose -f "${this.composeFile}" down -v --remove-orphans`);
|
|
210
|
+
this.log("Environment cleaned and stopped.", "success");
|
|
211
|
+
}
|
|
212
|
+
async reset() {
|
|
213
|
+
this.log("Performing Fast Browser Reset...", "info");
|
|
214
|
+
const androidCont = this.getContainerId("android-service");
|
|
215
|
+
if (!androidCont) {
|
|
216
|
+
this.log("Android container not running.", "error");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.log("Initiating fast reset (userspace reboot)...", "info");
|
|
220
|
+
try {
|
|
221
|
+
(0, import_child_process.execSync)(`docker exec "${androidCont}" adb shell reboot userspace`, { stdio: "ignore" });
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
this.log("Waiting for device to come online...", "info");
|
|
225
|
+
const start = Date.now();
|
|
226
|
+
let online = false;
|
|
227
|
+
while (Date.now() - start < 3e4) {
|
|
228
|
+
try {
|
|
229
|
+
const state = (0, import_child_process.execSync)(`docker exec "${androidCont}" adb get-state`, {
|
|
230
|
+
encoding: "utf-8"
|
|
231
|
+
}).trim();
|
|
232
|
+
if (state === "device") {
|
|
233
|
+
(0, import_child_process.execSync)(`docker exec "${androidCont}" adb shell echo ok`, { stdio: "ignore" });
|
|
234
|
+
online = true;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
240
|
+
}
|
|
241
|
+
if (!online) throw new Error("Timeout waiting for device after reboot");
|
|
242
|
+
this.log("Device online", "success");
|
|
243
|
+
this.log("Waiting for browser CDP...", "info");
|
|
244
|
+
const cdpStart = Date.now();
|
|
245
|
+
let cdpReady = false;
|
|
246
|
+
while (Date.now() - cdpStart < 3e4) {
|
|
247
|
+
try {
|
|
248
|
+
(0, import_child_process.execSync)(
|
|
249
|
+
`docker exec "${androidCont}" curl -s --connect-timeout 2 http://localhost:9224/json/version`,
|
|
250
|
+
{ stdio: "ignore" }
|
|
251
|
+
);
|
|
252
|
+
cdpReady = true;
|
|
253
|
+
break;
|
|
254
|
+
} catch {
|
|
255
|
+
if (Date.now() - cdpStart > 15e3) {
|
|
256
|
+
try {
|
|
257
|
+
(0, import_child_process.execSync)(
|
|
258
|
+
`docker exec "${androidCont}" adb shell am start -n com.wootzapp.web/com.aspect.chromium.ChromiumMain -a android.intent.action.VIEW -d 'about:blank'`,
|
|
259
|
+
{ stdio: "ignore" }
|
|
260
|
+
);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
266
|
+
}
|
|
267
|
+
if (!cdpReady) throw new Error("Timeout waiting for CDP");
|
|
268
|
+
this.log("Fast reset complete!", "success");
|
|
269
|
+
}
|
|
270
|
+
waitForLog(container, pattern, timeoutMs = 12e4) {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const start = Date.now();
|
|
273
|
+
const tail = (0, import_child_process.spawn)("docker", ["logs", "-f", container]);
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
tail.kill();
|
|
276
|
+
reject(new Error(`Timeout waiting for log pattern "${pattern}" in container ${container}`));
|
|
277
|
+
}, timeoutMs);
|
|
278
|
+
tail.stdout.on("data", (data) => {
|
|
279
|
+
if (data.toString().includes(pattern)) {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
tail.kill();
|
|
282
|
+
resolve();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
tail.stderr.on("data", (data) => {
|
|
286
|
+
if (data.toString().includes(pattern)) {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
tail.kill();
|
|
289
|
+
resolve();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
tail.on("error", (err) => {
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
reject(err);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/index.ts
|
|
301
|
+
var import_zod = require("zod");
|
|
302
|
+
var import_pino2 = __toESM(require("pino"), 1);
|
|
303
|
+
var DAEMON_PORT = 32001;
|
|
304
|
+
var WootzConnectionError = class extends Error {
|
|
305
|
+
constructor(message) {
|
|
306
|
+
super(message);
|
|
307
|
+
this.name = "WootzConnectionError";
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var WootzTimeoutError = class extends Error {
|
|
311
|
+
constructor(message) {
|
|
312
|
+
super(message);
|
|
313
|
+
this.name = "WootzTimeoutError";
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
var WootzValidationError = class extends Error {
|
|
317
|
+
constructor(message) {
|
|
318
|
+
super(message);
|
|
319
|
+
this.name = "WootzValidationError";
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var AgentOptionsSchema = import_zod.z.object({
|
|
323
|
+
dist: import_zod.z.boolean().optional().default(true),
|
|
324
|
+
logLevel: import_zod.z.enum(["debug", "info", "warn", "error", "silent"]).optional().default("info")
|
|
325
|
+
}).passthrough();
|
|
326
|
+
var WootzAgent = class {
|
|
327
|
+
options;
|
|
328
|
+
orchestrator;
|
|
329
|
+
logger;
|
|
330
|
+
/**
|
|
331
|
+
* Creates a new instance of the WootzAgent.
|
|
332
|
+
* @param options Configuration options.
|
|
333
|
+
* @throws {WootzValidationError} If the provided options do not match the expected schema.
|
|
334
|
+
*/
|
|
335
|
+
constructor(options = {}) {
|
|
336
|
+
const parsedOptions = AgentOptionsSchema.safeParse(options);
|
|
337
|
+
if (!parsedOptions.success) {
|
|
338
|
+
throw new WootzValidationError(`Invalid agent options: ${parsedOptions.error.message}`);
|
|
339
|
+
}
|
|
340
|
+
this.options = parsedOptions.data;
|
|
341
|
+
this.logger = (0, import_pino2.default)({
|
|
342
|
+
level: this.options.logLevel,
|
|
343
|
+
transport: this.options.logLevel !== "silent" ? {
|
|
344
|
+
target: "pino-pretty",
|
|
345
|
+
options: {
|
|
346
|
+
colorize: true,
|
|
347
|
+
translateTime: "HH:MM:ss Z",
|
|
348
|
+
ignore: "pid,hostname"
|
|
349
|
+
}
|
|
350
|
+
} : void 0
|
|
351
|
+
});
|
|
352
|
+
this.orchestrator = new Orchestrator(this.options.dist, this.logger);
|
|
353
|
+
}
|
|
354
|
+
// --- Lifecycle Management (Infrastructure) ---
|
|
355
|
+
/**
|
|
356
|
+
* Starts the underlying orchestrator and waits for the browser daemon to be ready.
|
|
357
|
+
* @throws {WootzTimeoutError} If the daemon fails to become available within the timeout.
|
|
358
|
+
*/
|
|
359
|
+
async start() {
|
|
360
|
+
await this.orchestrator.start();
|
|
361
|
+
await this.waitForDaemon();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Polls the daemon port until it accepts connections.
|
|
365
|
+
* @param timeoutMs The maximum amount of time (in ms) to wait for the daemon.
|
|
366
|
+
*/
|
|
367
|
+
async waitForDaemon(timeoutMs = 18e4) {
|
|
368
|
+
const start = Date.now();
|
|
369
|
+
while (Date.now() - start < timeoutMs) {
|
|
370
|
+
try {
|
|
371
|
+
await this.sendCommand({ action: "tab_list" });
|
|
372
|
+
return;
|
|
373
|
+
} catch (e) {
|
|
374
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
throw new WootzTimeoutError(
|
|
378
|
+
`Timed out waiting for Agent Daemon on port ${DAEMON_PORT} after ${timeoutMs / 1e3}s`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Stops the orchestrator and cleans up resources.
|
|
383
|
+
*/
|
|
384
|
+
async stop() {
|
|
385
|
+
await this.orchestrator.stop();
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Resets the environment completely.
|
|
389
|
+
*/
|
|
390
|
+
async reset() {
|
|
391
|
+
await this.orchestrator.reset();
|
|
392
|
+
}
|
|
393
|
+
// --- Core Browser Actions (Direct Daemon) ---
|
|
394
|
+
/**
|
|
395
|
+
* Navigates the current tab to a specified URL.
|
|
396
|
+
* @param url The URL to navigate to.
|
|
397
|
+
* @param waitUntil The load state to wait for ('load', 'domcontentloaded', 'networkidle').
|
|
398
|
+
*/
|
|
399
|
+
async navigate(url, waitUntil = "load") {
|
|
400
|
+
return await this.sendCommand({ action: "navigate", url, waitUntil });
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Clicks on an element matching the given selector.
|
|
404
|
+
* @param selector The CSS or semantic selector of the element to click.
|
|
405
|
+
*/
|
|
406
|
+
async click(selector) {
|
|
407
|
+
return await this.sendCommand({ action: "click", selector });
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Double-clicks on an element matching the given selector.
|
|
411
|
+
* @param selector The CSS or semantic selector.
|
|
412
|
+
*/
|
|
413
|
+
async doubleClick(selector) {
|
|
414
|
+
return await this.sendCommand({ action: "dblclick", selector });
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Types text into a form field character by character.
|
|
418
|
+
* @param selector The CSS or semantic selector of the input field.
|
|
419
|
+
* @param text The text to type.
|
|
420
|
+
* @param delay Delay between keystrokes in milliseconds.
|
|
421
|
+
*/
|
|
422
|
+
async type(selector, text, delay) {
|
|
423
|
+
return await this.sendCommand({ action: "type", selector, text, delay });
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Fills an input field directly with a value (faster than type).
|
|
427
|
+
* @param selector The CSS or semantic selector of the input field.
|
|
428
|
+
* @param value The value to fill.
|
|
429
|
+
*/
|
|
430
|
+
async fill(selector, value) {
|
|
431
|
+
return await this.sendCommand({ action: "fill", selector, value });
|
|
432
|
+
}
|
|
433
|
+
async check(selector) {
|
|
434
|
+
return await this.sendCommand({ action: "check", selector });
|
|
435
|
+
}
|
|
436
|
+
async uncheck(selector) {
|
|
437
|
+
return await this.sendCommand({ action: "uncheck", selector });
|
|
438
|
+
}
|
|
439
|
+
async hover(selector) {
|
|
440
|
+
return await this.sendCommand({ action: "hover", selector });
|
|
441
|
+
}
|
|
442
|
+
async focus(selector) {
|
|
443
|
+
return await this.sendCommand({ action: "focus", selector });
|
|
444
|
+
}
|
|
445
|
+
async press(key, selector) {
|
|
446
|
+
return await this.sendCommand({ action: "press", key, selector });
|
|
447
|
+
}
|
|
448
|
+
async selectOption(selector, value) {
|
|
449
|
+
return await this.sendCommand({ action: "select", selector, values: value });
|
|
450
|
+
}
|
|
451
|
+
// --- Introspection & State ---
|
|
452
|
+
/**
|
|
453
|
+
* Captures the full DOM/AXTree snapshot of the current page.
|
|
454
|
+
* @returns A string representation of the interactive accessibility tree.
|
|
455
|
+
*/
|
|
456
|
+
async snapshot() {
|
|
457
|
+
const result = await this.sendCommand({ action: "snapshot" }, true);
|
|
458
|
+
return result.snapshot || "";
|
|
459
|
+
}
|
|
460
|
+
async innerText(selector) {
|
|
461
|
+
const res = await this.sendCommand({ action: "innertext", selector }, true);
|
|
462
|
+
return res.text;
|
|
463
|
+
}
|
|
464
|
+
async innerHTML(selector) {
|
|
465
|
+
const res = await this.sendCommand({ action: "innerhtml", selector }, true);
|
|
466
|
+
return res.html;
|
|
467
|
+
}
|
|
468
|
+
async getAttribute(selector, attribute) {
|
|
469
|
+
const res = await this.sendCommand({ action: "getattribute", selector, attribute }, true);
|
|
470
|
+
return res.value;
|
|
471
|
+
}
|
|
472
|
+
async inputValue(selector) {
|
|
473
|
+
const res = await this.sendCommand({ action: "inputvalue", selector }, true);
|
|
474
|
+
return res.value;
|
|
475
|
+
}
|
|
476
|
+
async isVisible(selector) {
|
|
477
|
+
const res = await this.sendCommand({ action: "isvisible", selector }, true);
|
|
478
|
+
return res.visible;
|
|
479
|
+
}
|
|
480
|
+
async isEnabled(selector) {
|
|
481
|
+
const res = await this.sendCommand({ action: "isenabled", selector }, true);
|
|
482
|
+
return res.enabled;
|
|
483
|
+
}
|
|
484
|
+
async isChecked(selector) {
|
|
485
|
+
const res = await this.sendCommand({ action: "ischecked", selector }, true);
|
|
486
|
+
return res.checked;
|
|
487
|
+
}
|
|
488
|
+
// --- Waiting & Navigation ---
|
|
489
|
+
async waitForSelector(selector, state = "visible", timeout) {
|
|
490
|
+
return await this.sendCommand({ action: "wait", selector, state, timeout });
|
|
491
|
+
}
|
|
492
|
+
async waitForTimeout(ms) {
|
|
493
|
+
return await this.sendCommand({ action: "wait", timeout: ms });
|
|
494
|
+
}
|
|
495
|
+
async waitForLoadState(state = "load") {
|
|
496
|
+
return await this.sendCommand({ action: "waitforloadstate", state });
|
|
497
|
+
}
|
|
498
|
+
async reload() {
|
|
499
|
+
return await this.sendCommand({ action: "reload" });
|
|
500
|
+
}
|
|
501
|
+
async goBack() {
|
|
502
|
+
return await this.sendCommand({ action: "back" });
|
|
503
|
+
}
|
|
504
|
+
async goForward() {
|
|
505
|
+
return await this.sendCommand({ action: "forward" });
|
|
506
|
+
}
|
|
507
|
+
// --- Semantic Locators ---
|
|
508
|
+
async getByRole(role, name, _exact = false) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
"Locator builders (getByRole) are not fully supported in stateless mode yet. Use standard selectors."
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
// --- Utilities ---
|
|
514
|
+
/**
|
|
515
|
+
* Captures a screenshot of the current page.
|
|
516
|
+
* @param path Optional file path to save the screenshot.
|
|
517
|
+
*/
|
|
518
|
+
async screenshot(path2) {
|
|
519
|
+
const res = await this.sendCommand({ action: "screenshot", path: path2 }, true);
|
|
520
|
+
return res.path;
|
|
521
|
+
}
|
|
522
|
+
async scroll(x, y) {
|
|
523
|
+
return await this.sendCommand({ action: "scroll", x, y });
|
|
524
|
+
}
|
|
525
|
+
async evaluate(script) {
|
|
526
|
+
const res = await this.sendCommand({ action: "evaluate", script }, true);
|
|
527
|
+
return res.result;
|
|
528
|
+
}
|
|
529
|
+
// --- Tab Management ---
|
|
530
|
+
async newTab(url) {
|
|
531
|
+
return await this.sendCommand({ action: "tab_new", url });
|
|
532
|
+
}
|
|
533
|
+
async switchTab(index) {
|
|
534
|
+
return await this.sendCommand({ action: "tab_switch", index });
|
|
535
|
+
}
|
|
536
|
+
async closeTab(index) {
|
|
537
|
+
return await this.sendCommand({ action: "tab_close", index });
|
|
538
|
+
}
|
|
539
|
+
async listTabs() {
|
|
540
|
+
const res = await this.sendCommand({ action: "tab_list" }, true);
|
|
541
|
+
return res.tabs;
|
|
542
|
+
}
|
|
543
|
+
// --- Generic Execution ---
|
|
544
|
+
async command(action, ...args2) {
|
|
545
|
+
const cmd = { action };
|
|
546
|
+
if (action === "open" || action === "navigate") {
|
|
547
|
+
cmd.action = "navigate";
|
|
548
|
+
cmd.url = args2[0];
|
|
549
|
+
} else if (action === "click") {
|
|
550
|
+
cmd.selector = args2[0];
|
|
551
|
+
} else if (action === "type") {
|
|
552
|
+
cmd.selector = args2[0];
|
|
553
|
+
cmd.text = args2[1];
|
|
554
|
+
} else if (action === "press") {
|
|
555
|
+
cmd.key = args2[0];
|
|
556
|
+
} else if (action === "scroll") {
|
|
557
|
+
cmd.x = parseInt(args2[0] || "0");
|
|
558
|
+
cmd.y = parseInt(args2[1] || "0");
|
|
559
|
+
} else if (action === "wait") {
|
|
560
|
+
if (/^\d+$/.test(args2[0])) cmd.timeout = parseInt(args2[0]);
|
|
561
|
+
else cmd.selector = args2[0];
|
|
562
|
+
} else {
|
|
563
|
+
if (args2[0]) cmd.selector = args2[0];
|
|
564
|
+
if (args2[1]) cmd.text = args2[1] || args2[1];
|
|
565
|
+
}
|
|
566
|
+
const res = await this.sendCommand(cmd, true);
|
|
567
|
+
if (typeof res === "object") {
|
|
568
|
+
if (res.text) return res.text;
|
|
569
|
+
if (res.url) return res.url;
|
|
570
|
+
if (res.snapshot) return res.snapshot;
|
|
571
|
+
return JSON.stringify(res);
|
|
572
|
+
}
|
|
573
|
+
return String(res);
|
|
574
|
+
}
|
|
575
|
+
sendCommand(command2, returnData = false) {
|
|
576
|
+
return new Promise((resolve, reject) => {
|
|
577
|
+
if (!command2.id) command2.id = (0, import_crypto.randomUUID)();
|
|
578
|
+
const client = new net.Socket();
|
|
579
|
+
client.connect(DAEMON_PORT, "127.0.0.1", () => {
|
|
580
|
+
client.write(JSON.stringify(command2) + "\n");
|
|
581
|
+
});
|
|
582
|
+
let responseBuffer = "";
|
|
583
|
+
client.on("data", (data) => {
|
|
584
|
+
responseBuffer += data.toString();
|
|
585
|
+
if (responseBuffer.includes("\n")) {
|
|
586
|
+
client.end();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
client.on("end", () => {
|
|
590
|
+
try {
|
|
591
|
+
const response = JSON.parse(responseBuffer.trim());
|
|
592
|
+
if (response.success) {
|
|
593
|
+
resolve(returnData ? response.data : "OK");
|
|
594
|
+
} else {
|
|
595
|
+
reject(new Error(response.error || "Unknown error"));
|
|
596
|
+
}
|
|
597
|
+
} catch (e) {
|
|
598
|
+
reject(new Error(`Invalid response from daemon: ${responseBuffer}`));
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
client.on("error", (err) => {
|
|
602
|
+
reject(new WootzConnectionError(`Failed to connect to agent daemon (is it running?): ${err.message}`));
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/cli.ts
|
|
609
|
+
var args = process.argv.slice(2);
|
|
610
|
+
var command = args.find((arg) => !arg.startsWith("-"));
|
|
611
|
+
if (!command || command === "help") {
|
|
612
|
+
console.log(`
|
|
613
|
+
WootzApp Agent Browser CLI
|
|
614
|
+
|
|
615
|
+
Usage:
|
|
616
|
+
agent-browser start Initialize and start the environment
|
|
617
|
+
agent-browser stop Stop the environment
|
|
618
|
+
agent-browser reset Fast reset the browser state
|
|
619
|
+
agent-browser <cmd> Run an agent command (e.g. open https://google.com)
|
|
620
|
+
|
|
621
|
+
Options:
|
|
622
|
+
--dist Use pre-built images from Docker Hub (default)
|
|
623
|
+
--local Build images locally from source
|
|
624
|
+
`);
|
|
625
|
+
process.exit(0);
|
|
626
|
+
}
|
|
627
|
+
var isLocal = args.includes("--local");
|
|
628
|
+
var filteredArgs = args.filter((arg) => arg !== "--local" && arg !== "--dist");
|
|
629
|
+
var agent = new WootzAgent({ dist: !isLocal });
|
|
630
|
+
async function main() {
|
|
631
|
+
try {
|
|
632
|
+
switch (command) {
|
|
633
|
+
case "start":
|
|
634
|
+
await agent.start();
|
|
635
|
+
break;
|
|
636
|
+
case "stop":
|
|
637
|
+
await agent.stop();
|
|
638
|
+
break;
|
|
639
|
+
case "reset":
|
|
640
|
+
await agent.reset();
|
|
641
|
+
break;
|
|
642
|
+
default:
|
|
643
|
+
const result = await agent.command(command, ...filteredArgs.slice(1));
|
|
644
|
+
console.log(result);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
} catch (error) {
|
|
648
|
+
console.error(`
|
|
649
|
+
\u274C Error: ${error.message || error}`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
main();
|
|
654
|
+
//# sourceMappingURL=cli.cjs.map
|