@agentuity/browzer 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/LICENSE.md +21 -0
- package/README.md +103 -0
- package/dist/cli/agent.js +63 -0
- package/dist/cli/argv.js +103 -0
- package/dist/cli/browzer.js +95 -0
- package/dist/cli/collect.js +166 -0
- package/dist/cli/commands.js +128 -0
- package/dist/cli/paths.js +45 -0
- package/dist/cli/replay-html.js +308 -0
- package/dist/cli/replay.js +89 -0
- package/dist/cli/skills.js +106 -0
- package/dist/cli/state.js +47 -0
- package/dist/cli/stream.js +55 -0
- package/dist/cli/summary.js +116 -0
- package/dist/lib/trace-store.js +227 -0
- package/docs/replay.png +0 -0
- package/package.json +41 -0
- package/skills/browzer/SKILL.md +49 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agentuity, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Browzer
|
|
2
|
+
|
|
3
|
+
`browzer` is [agent-browser](https://agent-browser.dev) with a trace. Same commands, same snapshot-and-click loop. Each session is saved so you can inspect it later or open a local HTML replay.
|
|
4
|
+
|
|
5
|
+
Live watching stays with `agent-browser dashboard`. Browzer is the bundle you keep after `close`.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Needs Node 18+, [agent-browser](https://agent-browser.dev) on `PATH`, and [ffmpeg](https://ffmpeg.org/) on `PATH` (for video).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -g @agentuity/browzer
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Confirm:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
browzer help
|
|
21
|
+
agent-browser --version
|
|
22
|
+
ffmpeg -version
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install the Agent Skill so Claude Code, Cursor, Codex, Grok, and others treat `browzer` as the browser CLI when a recording is wanted:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
browzer skills install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export AGENT_BROWSER_SESSION=demo
|
|
35
|
+
export AGENT_BROWSER_IDLE_TIMEOUT_MS=0
|
|
36
|
+
|
|
37
|
+
browzer --session demo open https://example.com
|
|
38
|
+
browzer --session demo snapshot -i
|
|
39
|
+
browzer --session demo find text "Learn more" click
|
|
40
|
+
browzer --session demo close
|
|
41
|
+
|
|
42
|
+
browzer traces
|
|
43
|
+
browzer inspect demo
|
|
44
|
+
browzer replay demo
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Every `browzer` invocation except `dashboard`, `mcp`, `doctor`, `install`, `upgrade`, and `session` metadata is the same argv you would have passed to `agent-browser`. Do not also run `agent-browser` on that `--session`.
|
|
48
|
+
|
|
49
|
+
## How a trace is captured
|
|
50
|
+
|
|
51
|
+
On the first successful command for a session, Browzer starts:
|
|
52
|
+
|
|
53
|
+
- `record start` → `session.mp4`
|
|
54
|
+
- `network har start --content text` → `network.har`
|
|
55
|
+
|
|
56
|
+
While commands run, it appends:
|
|
57
|
+
|
|
58
|
+
- `commands.jsonl` — argv, duration, exit, stdout/stderr
|
|
59
|
+
- `events.jsonl` — stream `command` / `result` / `url` / `tabs`
|
|
60
|
+
- `console.jsonl` — console and page errors from the stream
|
|
61
|
+
|
|
62
|
+
On `close` it dumps `console.json`, `errors.json`, `snapshot.txt`, `poster.jpg`, stops recording and HAR, and writes `summary.md`.
|
|
63
|
+
|
|
64
|
+
Traces are written under the current directory as `<id>/` unless you pass `--output` / `-o` or set `BROWZER_OUTPUT`. Session bookkeeping still uses `~/.browzer/active` (override the home with `BROWZER_HOME`). Override the wrapped binary with `AGENT_BROWSER_BIN`.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
browzer --output ./artifacts --session demo open https://example.com
|
|
68
|
+
browzer --output ./artifacts traces
|
|
69
|
+
browzer --output ./artifacts replay demo
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Commands Browzer adds
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `browzer traces` | List traces |
|
|
77
|
+
| `browzer traces --json` | Same, JSON |
|
|
78
|
+
| `browzer inspect [id]` | Print `summary.md` (latest if omitted) |
|
|
79
|
+
| `browzer replay [id\|path]` | Write `replay.html` next to the video and open it |
|
|
80
|
+
| `browzer skills` | List the bundled skill |
|
|
81
|
+
| `browzer skills get` | Print `SKILL.md` matching this CLI |
|
|
82
|
+
| `browzer skills install` | Copy the skill into `~/.agents`, plus Claude / Cursor / Codex / Grok if those dirs exist |
|
|
83
|
+
| `browzer skills install --project` | Copy into this repo’s agent skill folders |
|
|
84
|
+
|
|
85
|
+
Everything else is forwarded to `agent-browser`.
|
|
86
|
+
|
|
87
|
+
## Replay
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
browzer replay # latest
|
|
91
|
+
browzer replay demo # id, prefix, or session suffix
|
|
92
|
+
browzer replay ./20260917-185027-long-demo
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The page is a static HTML file beside `session.mp4`. No app server. The URL bar follows the playhead. Steps seek the video. Console, network, errors, and snapshot sit behind **Show details**.
|
|
96
|
+
|
|
97
|
+
## After a failure
|
|
98
|
+
|
|
99
|
+
Agents should read `browzer inspect` first (failed commands, HAR 4xx/5xx, console errors). Humans open `browzer replay`. Live CDP-style inspection during a run is `agent-browser dashboard start` at http://localhost:4848.
|
|
100
|
+
|
|
101
|
+
## Skill
|
|
102
|
+
|
|
103
|
+
Canonical file: [`skills/browzer/SKILL.md`](skills/browzer/SKILL.md). Same Agent Skills format used by Claude Code, Cursor, Codex, and Grok. It is a prefix swap: same commands as agent-browser, invoke `browzer`. Snapshot/click/fill details stay in the agent-browser core skill.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_BROWSER_BIN = void 0;
|
|
4
|
+
exports.runAgentBrowser = runAgentBrowser;
|
|
5
|
+
exports.parseWrappedJson = parseWrappedJson;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
exports.AGENT_BROWSER_BIN = process.env.AGENT_BROWSER_BIN ?? "agent-browser";
|
|
8
|
+
function runAgentBrowser(args, opts) {
|
|
9
|
+
const tee = opts?.tee ?? false;
|
|
10
|
+
const started = Date.now();
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = (0, node_child_process_1.spawn)(exports.AGENT_BROWSER_BIN, args, {
|
|
13
|
+
env: process.env,
|
|
14
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
let stdout = "";
|
|
17
|
+
let stderr = "";
|
|
18
|
+
let settled = false;
|
|
19
|
+
const finish = (code) => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
23
|
+
if (timer)
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
resolve({ code, stdout, stderr, durationMs: Date.now() - started });
|
|
26
|
+
};
|
|
27
|
+
const timer = opts?.timeoutMs != null
|
|
28
|
+
? setTimeout(() => {
|
|
29
|
+
child.kill("SIGTERM");
|
|
30
|
+
reject(new Error(`agent-browser timed out: ${args.join(" ")}`));
|
|
31
|
+
}, opts.timeoutMs)
|
|
32
|
+
: null;
|
|
33
|
+
child.stdout.on("data", (chunk) => {
|
|
34
|
+
stdout += chunk.toString("utf8");
|
|
35
|
+
if (tee)
|
|
36
|
+
process.stdout.write(chunk);
|
|
37
|
+
});
|
|
38
|
+
child.stderr.on("data", (chunk) => {
|
|
39
|
+
stderr += chunk.toString("utf8");
|
|
40
|
+
if (tee)
|
|
41
|
+
process.stderr.write(chunk);
|
|
42
|
+
});
|
|
43
|
+
child.on("error", (err) => {
|
|
44
|
+
if (err.code === "ENOENT") {
|
|
45
|
+
reject(new Error("agent-browser not found on PATH. Install it globally (npm i -g agent-browser) or set AGENT_BROWSER_BIN."));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
reject(err);
|
|
49
|
+
});
|
|
50
|
+
child.on("close", (code) => finish(code ?? 1));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function parseWrappedJson(text) {
|
|
54
|
+
const start = text.indexOf("{");
|
|
55
|
+
if (start < 0)
|
|
56
|
+
return null;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(text.slice(start));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/cli/argv.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLOSE_COMMANDS = exports.SESSION_META_COMMANDS = exports.PASSTHROUGH_COMMANDS = exports.BROWZER_COMMANDS = void 0;
|
|
4
|
+
exports.parseArgv = parseArgv;
|
|
5
|
+
exports.isPassthrough = isPassthrough;
|
|
6
|
+
const FLAGS_WITH_VALUE = new Set([
|
|
7
|
+
"--session",
|
|
8
|
+
"--cdp",
|
|
9
|
+
"--profile",
|
|
10
|
+
"--idle-timeout",
|
|
11
|
+
"--config",
|
|
12
|
+
"--allowed-domains",
|
|
13
|
+
"--executable-path",
|
|
14
|
+
"--user-agent",
|
|
15
|
+
"--proxy",
|
|
16
|
+
"--proxy-bypass",
|
|
17
|
+
"--headers",
|
|
18
|
+
"--storage-state",
|
|
19
|
+
"--init-script",
|
|
20
|
+
"--tools",
|
|
21
|
+
"--port",
|
|
22
|
+
"--allowed-origins",
|
|
23
|
+
"--output",
|
|
24
|
+
"-o",
|
|
25
|
+
"--provider",
|
|
26
|
+
]);
|
|
27
|
+
exports.BROWZER_COMMANDS = new Set(["traces", "inspect", "replay", "help", "skills"]);
|
|
28
|
+
exports.PASSTHROUGH_COMMANDS = new Set([
|
|
29
|
+
"dashboard",
|
|
30
|
+
"mcp",
|
|
31
|
+
"doctor",
|
|
32
|
+
"install",
|
|
33
|
+
"upgrade",
|
|
34
|
+
"plugin",
|
|
35
|
+
"help",
|
|
36
|
+
"--help",
|
|
37
|
+
"-h",
|
|
38
|
+
"--version",
|
|
39
|
+
"-V",
|
|
40
|
+
"-v",
|
|
41
|
+
]);
|
|
42
|
+
exports.SESSION_META_COMMANDS = new Set(["session"]);
|
|
43
|
+
exports.CLOSE_COMMANDS = new Set(["close", "quit", "exit"]);
|
|
44
|
+
function parseArgv(argv) {
|
|
45
|
+
const forwarded = [];
|
|
46
|
+
let session = process.env.AGENT_BROWSER_SESSION?.trim() || "default";
|
|
47
|
+
let json = false;
|
|
48
|
+
let command = null;
|
|
49
|
+
let output = process.env.BROWZER_OUTPUT?.trim() || undefined;
|
|
50
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
+
const token = argv[i] ?? "";
|
|
52
|
+
if ((token === "--output" || token === "-o") && argv[i + 1]) {
|
|
53
|
+
output = argv[i + 1];
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (token.startsWith("--output=")) {
|
|
58
|
+
output = token.slice("--output=".length);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (token === "--session" && argv[i + 1]) {
|
|
62
|
+
session = argv[i + 1];
|
|
63
|
+
forwarded.push(token, argv[i + 1]);
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (token.startsWith("--session=")) {
|
|
68
|
+
session = token.slice("--session=".length);
|
|
69
|
+
forwarded.push(token);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (token === "--json") {
|
|
73
|
+
json = true;
|
|
74
|
+
forwarded.push(token);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (token === "--headed" && (argv[i + 1] === "true" || argv[i + 1] === "false")) {
|
|
78
|
+
forwarded.push(token, argv[i + 1]);
|
|
79
|
+
i += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
forwarded.push(token);
|
|
83
|
+
if (token.startsWith("-")) {
|
|
84
|
+
if (FLAGS_WITH_VALUE.has(token) && argv[i + 1] && !argv[i + 1].startsWith("-")) {
|
|
85
|
+
forwarded.push(argv[i + 1]);
|
|
86
|
+
i += 1;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!command)
|
|
91
|
+
command = token;
|
|
92
|
+
}
|
|
93
|
+
return { session, command, forwarded, json, output };
|
|
94
|
+
}
|
|
95
|
+
function isPassthrough(command) {
|
|
96
|
+
if (!command)
|
|
97
|
+
return true;
|
|
98
|
+
if (exports.PASSTHROUGH_COMMANDS.has(command))
|
|
99
|
+
return true;
|
|
100
|
+
if (exports.SESSION_META_COMMANDS.has(command))
|
|
101
|
+
return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const argv_1 = require("./argv");
|
|
5
|
+
const agent_1 = require("./agent");
|
|
6
|
+
const commands_1 = require("./commands");
|
|
7
|
+
const replay_1 = require("./replay");
|
|
8
|
+
const skills_1 = require("./skills");
|
|
9
|
+
const collect_1 = require("./collect");
|
|
10
|
+
const state_1 = require("./state");
|
|
11
|
+
async function main() {
|
|
12
|
+
const argv = process.argv.slice(2);
|
|
13
|
+
const parsed = (0, argv_1.parseArgv)(argv);
|
|
14
|
+
if (parsed.command && argv_1.BROWZER_COMMANDS.has(parsed.command)) {
|
|
15
|
+
if (parsed.command === "help") {
|
|
16
|
+
process.stdout.write((0, commands_1.helpText)());
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (parsed.command === "traces") {
|
|
20
|
+
await (0, commands_1.tracesCommand)(parsed.json, parsed.output);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (parsed.command === "inspect") {
|
|
24
|
+
const index = argv.indexOf("inspect");
|
|
25
|
+
const id = argv.slice(index + 1).find((token) => !token.startsWith("-"));
|
|
26
|
+
await (0, commands_1.inspectCommand)(id, parsed.output);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (parsed.command === "replay") {
|
|
30
|
+
const index = argv.indexOf("replay");
|
|
31
|
+
const target = argv.slice(index + 1).find((token) => !token.startsWith("-"));
|
|
32
|
+
await (0, replay_1.replayCommand)(target, parsed.output);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (parsed.command === "skills") {
|
|
36
|
+
await (0, skills_1.skillsCommand)(argv, parsed.json);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if ((0, argv_1.isPassthrough)(parsed.command)) {
|
|
41
|
+
const result = await (0, agent_1.runAgentBrowser)(parsed.forwarded, { tee: true });
|
|
42
|
+
process.exit(result.code);
|
|
43
|
+
}
|
|
44
|
+
const startUrl = commandUrl(parsed.forwarded, parsed.command);
|
|
45
|
+
const closing = argv_1.CLOSE_COMMANDS.has(parsed.command ?? "");
|
|
46
|
+
if (closing && parsed.forwarded.includes("--all")) {
|
|
47
|
+
for (const active of await (0, state_1.listActive)()) {
|
|
48
|
+
try {
|
|
49
|
+
await (0, collect_1.finalize)(active);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
process.stderr.write(`browzer: failed to finalize ${active.id}: ${String(err)}\n`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const result = await (0, agent_1.runAgentBrowser)(parsed.forwarded, { tee: true });
|
|
56
|
+
process.exit(result.code);
|
|
57
|
+
}
|
|
58
|
+
if (closing) {
|
|
59
|
+
const active = await (0, state_1.loadActive)(parsed.session);
|
|
60
|
+
if (active) {
|
|
61
|
+
try {
|
|
62
|
+
await (0, collect_1.finalize)(active);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
process.stderr.write(`browzer: finalize failed: ${String(err)}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const result = await (0, agent_1.runAgentBrowser)(parsed.forwarded, { tee: true });
|
|
69
|
+
process.exit(result.code);
|
|
70
|
+
}
|
|
71
|
+
const trace = await (0, collect_1.ensureTrace)(parsed.session, startUrl, parsed.output);
|
|
72
|
+
const result = await (0, collect_1.wrapCommand)(trace, parsed.forwarded);
|
|
73
|
+
if (result.code === 0) {
|
|
74
|
+
const current = (await (0, state_1.loadActive)(parsed.session)) ?? trace;
|
|
75
|
+
try {
|
|
76
|
+
await (0, collect_1.startCapture)(current);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
process.stderr.write(`browzer: capture not started: ${String(err)}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
process.exit(result.code);
|
|
83
|
+
}
|
|
84
|
+
function commandUrl(argv, command) {
|
|
85
|
+
if (command !== "open" && command !== "goto" && command !== "navigate")
|
|
86
|
+
return undefined;
|
|
87
|
+
const index = argv.findIndex((token) => token === command);
|
|
88
|
+
if (index < 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
return argv.slice(index + 1).find((token) => !token.startsWith("-"));
|
|
91
|
+
}
|
|
92
|
+
main().catch((err) => {
|
|
93
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureTrace = ensureTrace;
|
|
7
|
+
exports.logCommand = logCommand;
|
|
8
|
+
exports.startCapture = startCapture;
|
|
9
|
+
exports.wrapCommand = wrapCommand;
|
|
10
|
+
exports.finalize = finalize;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const agent_1 = require("./agent");
|
|
14
|
+
const paths_1 = require("./paths");
|
|
15
|
+
const summary_1 = require("./summary");
|
|
16
|
+
const stream_1 = require("./stream");
|
|
17
|
+
const state_1 = require("./state");
|
|
18
|
+
const MAX_CAPTURE = 24_000;
|
|
19
|
+
async function ensureTrace(session, startUrl, output) {
|
|
20
|
+
const existing = await (0, state_1.loadActive)(session);
|
|
21
|
+
if (existing)
|
|
22
|
+
return existing;
|
|
23
|
+
const id = (0, paths_1.newTraceId)(session);
|
|
24
|
+
const dir = node_path_1.default.join((0, paths_1.tracesDir)(output), id);
|
|
25
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
26
|
+
const trace = {
|
|
27
|
+
id,
|
|
28
|
+
dir,
|
|
29
|
+
session,
|
|
30
|
+
startedAt: Date.now(),
|
|
31
|
+
recording: false,
|
|
32
|
+
har: false,
|
|
33
|
+
streamPort: null,
|
|
34
|
+
startUrl,
|
|
35
|
+
};
|
|
36
|
+
await (0, state_1.saveActive)(trace);
|
|
37
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(dir, "meta.json"), JSON.stringify({ id, session, startedAt: trace.startedAt, startUrl, status: "active" }, null, 2));
|
|
38
|
+
return trace;
|
|
39
|
+
}
|
|
40
|
+
async function logCommand(trace, row) {
|
|
41
|
+
const line = {
|
|
42
|
+
ts: Date.now(),
|
|
43
|
+
session: trace.session,
|
|
44
|
+
argv: row.argv,
|
|
45
|
+
durationMs: row.durationMs,
|
|
46
|
+
exitCode: row.exitCode,
|
|
47
|
+
stdout: clip(row.stdout),
|
|
48
|
+
stderr: clip(row.stderr),
|
|
49
|
+
};
|
|
50
|
+
await (0, promises_1.appendFile)(node_path_1.default.join(trace.dir, "commands.jsonl"), `${JSON.stringify(line)}\n`);
|
|
51
|
+
}
|
|
52
|
+
async function startCapture(trace) {
|
|
53
|
+
const status = await abJson(trace.session, [
|
|
54
|
+
"stream",
|
|
55
|
+
"status",
|
|
56
|
+
]);
|
|
57
|
+
if (status?.port) {
|
|
58
|
+
trace.streamPort = status.port;
|
|
59
|
+
}
|
|
60
|
+
if (!trace.recording && !trace.recordAttempted) {
|
|
61
|
+
const video = node_path_1.default.join(trace.dir, "session.mp4");
|
|
62
|
+
const started = await abJson(trace.session, ["record", "start", video, "--fps", "12"]);
|
|
63
|
+
trace.recording = Boolean(started);
|
|
64
|
+
trace.recordAttempted = true;
|
|
65
|
+
}
|
|
66
|
+
if (!trace.har && !trace.harAttempted) {
|
|
67
|
+
const started = await runSilent(trace.session, ["network", "har", "start", "--content", "text"]);
|
|
68
|
+
trace.har = started.code === 0;
|
|
69
|
+
trace.harAttempted = true;
|
|
70
|
+
}
|
|
71
|
+
await (0, state_1.saveActive)(trace);
|
|
72
|
+
return trace;
|
|
73
|
+
}
|
|
74
|
+
async function wrapCommand(trace, forwarded) {
|
|
75
|
+
const writes = [];
|
|
76
|
+
const run = () => (0, agent_1.runAgentBrowser)(forwarded, { tee: true });
|
|
77
|
+
const result = await (0, stream_1.collectDuring)(trace.streamPort, run, (event) => {
|
|
78
|
+
writes.push((0, promises_1.appendFile)(node_path_1.default.join(trace.dir, "events.jsonl"), `${JSON.stringify(event)}\n`));
|
|
79
|
+
if (event.type === "url" && typeof event.url === "string") {
|
|
80
|
+
trace.lastUrl = event.url;
|
|
81
|
+
}
|
|
82
|
+
if (event.type === "console" || event.type === "page_error") {
|
|
83
|
+
writes.push((0, promises_1.appendFile)(node_path_1.default.join(trace.dir, "console.jsonl"), `${JSON.stringify(event)}\n`));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
await Promise.all(writes);
|
|
87
|
+
await logCommand(trace, {
|
|
88
|
+
argv: forwarded,
|
|
89
|
+
durationMs: result.durationMs,
|
|
90
|
+
exitCode: result.code,
|
|
91
|
+
stdout: result.stdout,
|
|
92
|
+
stderr: result.stderr,
|
|
93
|
+
});
|
|
94
|
+
await (0, state_1.saveActive)(trace);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
async function finalize(trace) {
|
|
98
|
+
const url = await textOut(trace.session, ["get", "url"]);
|
|
99
|
+
const title = await textOut(trace.session, ["get", "title"]);
|
|
100
|
+
if (url)
|
|
101
|
+
trace.lastUrl = url;
|
|
102
|
+
await dumpJson(trace.session, ["--json", "console"], node_path_1.default.join(trace.dir, "console.json"));
|
|
103
|
+
await dumpJson(trace.session, ["--json", "errors"], node_path_1.default.join(trace.dir, "errors.json"));
|
|
104
|
+
await dumpText(trace.session, ["snapshot", "-i"], node_path_1.default.join(trace.dir, "snapshot.txt"));
|
|
105
|
+
await runSilent(trace.session, [
|
|
106
|
+
"screenshot",
|
|
107
|
+
"--screenshot-format",
|
|
108
|
+
"jpeg",
|
|
109
|
+
"--screenshot-quality",
|
|
110
|
+
"70",
|
|
111
|
+
node_path_1.default.join(trace.dir, "poster.jpg"),
|
|
112
|
+
]);
|
|
113
|
+
if (trace.recording) {
|
|
114
|
+
await runSilent(trace.session, ["record", "stop"]);
|
|
115
|
+
trace.recording = false;
|
|
116
|
+
}
|
|
117
|
+
if (trace.har) {
|
|
118
|
+
await runSilent(trace.session, ["network", "har", "stop", node_path_1.default.join(trace.dir, "network.har")]);
|
|
119
|
+
trace.har = false;
|
|
120
|
+
}
|
|
121
|
+
await (0, summary_1.writeSummary)(trace, { title, url: trace.lastUrl || trace.startUrl });
|
|
122
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(trace.dir, "meta.json"), JSON.stringify({
|
|
123
|
+
id: trace.id,
|
|
124
|
+
session: trace.session,
|
|
125
|
+
startedAt: trace.startedAt,
|
|
126
|
+
endedAt: Date.now(),
|
|
127
|
+
startUrl: trace.startUrl,
|
|
128
|
+
lastUrl: trace.lastUrl || url,
|
|
129
|
+
title,
|
|
130
|
+
status: "complete",
|
|
131
|
+
dir: trace.dir,
|
|
132
|
+
}, null, 2));
|
|
133
|
+
await (0, state_1.clearActive)(trace.session);
|
|
134
|
+
}
|
|
135
|
+
async function abJson(session, args) {
|
|
136
|
+
const result = await runSilent(session, ["--json", ...args]);
|
|
137
|
+
const parsed = (0, agent_1.parseWrappedJson)(result.stdout || result.stderr);
|
|
138
|
+
if (!parsed?.success)
|
|
139
|
+
return null;
|
|
140
|
+
return parsed.data ?? null;
|
|
141
|
+
}
|
|
142
|
+
async function dumpJson(session, args, file) {
|
|
143
|
+
const result = await runSilent(session, args);
|
|
144
|
+
const text = result.stdout.trim() || result.stderr.trim();
|
|
145
|
+
if (text)
|
|
146
|
+
await (0, promises_1.writeFile)(file, text);
|
|
147
|
+
}
|
|
148
|
+
async function dumpText(session, args, file) {
|
|
149
|
+
const result = await runSilent(session, args);
|
|
150
|
+
const text = result.stdout.trim() || result.stderr.trim();
|
|
151
|
+
if (text)
|
|
152
|
+
await (0, promises_1.writeFile)(file, `${text}\n`);
|
|
153
|
+
}
|
|
154
|
+
async function textOut(session, args) {
|
|
155
|
+
const result = await runSilent(session, args);
|
|
156
|
+
const text = result.stdout.trim();
|
|
157
|
+
return text || undefined;
|
|
158
|
+
}
|
|
159
|
+
async function runSilent(session, args) {
|
|
160
|
+
return (0, agent_1.runAgentBrowser)(["--session", session, ...args], { tee: false, timeoutMs: 30_000 });
|
|
161
|
+
}
|
|
162
|
+
function clip(text) {
|
|
163
|
+
if (text.length <= MAX_CAPTURE)
|
|
164
|
+
return text;
|
|
165
|
+
return `${text.slice(0, MAX_CAPTURE)}\n…truncated`;
|
|
166
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.tracesCommand = tracesCommand;
|
|
7
|
+
exports.inspectCommand = inspectCommand;
|
|
8
|
+
exports.helpText = helpText;
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const promises_1 = require("node:fs/promises");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const paths_1 = require("./paths");
|
|
13
|
+
async function tracesCommand(json, output) {
|
|
14
|
+
const dir = (0, paths_1.tracesDir)(output);
|
|
15
|
+
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
16
|
+
if (json) {
|
|
17
|
+
process.stdout.write(`${JSON.stringify({ traces: [] })}\n`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
process.stdout.write("No traces yet. Run a session with browzer <agent-browser command>.\n");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const ids = (await (0, promises_1.readdir)(dir)).filter((name) => (0, node_fs_1.existsSync)(node_path_1.default.join(dir, name, "meta.json")));
|
|
24
|
+
const traces = await Promise.all(ids.map(async (id) => {
|
|
25
|
+
const meta = JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(dir, id, "meta.json"), "utf8"));
|
|
26
|
+
const info = await (0, promises_1.stat)(node_path_1.default.join(dir, id));
|
|
27
|
+
return { ...meta, id, mtime: info.mtimeMs, dir: node_path_1.default.join(dir, id) };
|
|
28
|
+
}));
|
|
29
|
+
traces.sort((a, b) => (b.startedAt ?? b.mtime) - (a.startedAt ?? a.mtime));
|
|
30
|
+
if (json) {
|
|
31
|
+
process.stdout.write(`${JSON.stringify({ traces }, null, 2)}\n`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (traces.length === 0) {
|
|
35
|
+
process.stdout.write("No traces yet.\n");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const trace of traces) {
|
|
39
|
+
const when = trace.startedAt ? new Date(trace.startedAt).toISOString() : "";
|
|
40
|
+
process.stdout.write(`${trace.id} ${trace.status ?? "?"} ${trace.session ?? ""} ${trace.lastUrl ?? ""} ${when}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function inspectCommand(idArg, output) {
|
|
44
|
+
const root = (0, paths_1.tracesDir)(output);
|
|
45
|
+
const id = idArg || (await latestId(root));
|
|
46
|
+
if (!id) {
|
|
47
|
+
process.stderr.write("No traces found.\n");
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const dir = node_path_1.default.join(root, id);
|
|
52
|
+
const matches = (0, node_fs_1.existsSync)(dir) ? [id] : await matchPrefix(id, root);
|
|
53
|
+
const resolved = matches.length <= 1 ? matches[0] : await newest(matches, root);
|
|
54
|
+
if (!resolved) {
|
|
55
|
+
process.stderr.write(`Trace not found: ${id}\n`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const resolvedDir = node_path_1.default.join(root, resolved);
|
|
60
|
+
const summaryPath = node_path_1.default.join(resolvedDir, "summary.md");
|
|
61
|
+
const metaPath = node_path_1.default.join(resolvedDir, "meta.json");
|
|
62
|
+
process.stdout.write(`trace: ${resolved}\n`);
|
|
63
|
+
process.stdout.write(`dir: ${resolvedDir}\n\n`);
|
|
64
|
+
if ((0, node_fs_1.existsSync)(summaryPath)) {
|
|
65
|
+
process.stdout.write(await (0, promises_1.readFile)(summaryPath, "utf8"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if ((0, node_fs_1.existsSync)(metaPath)) {
|
|
69
|
+
process.stdout.write(`${await (0, promises_1.readFile)(metaPath, "utf8")}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function latestId(dir) {
|
|
73
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
74
|
+
return null;
|
|
75
|
+
const ids = await (0, promises_1.readdir)(dir);
|
|
76
|
+
let best = null;
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
try {
|
|
79
|
+
const info = await (0, promises_1.stat)(node_path_1.default.join(dir, id));
|
|
80
|
+
if (!best || info.mtimeMs > best.mtime)
|
|
81
|
+
best = { id, mtime: info.mtimeMs };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* skip */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return best?.id ?? null;
|
|
88
|
+
}
|
|
89
|
+
async function matchPrefix(prefix, dir) {
|
|
90
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
91
|
+
return [];
|
|
92
|
+
const ids = await (0, promises_1.readdir)(dir);
|
|
93
|
+
return ids.filter((id) => id === prefix || id.startsWith(prefix) || id.endsWith(`-${prefix}`));
|
|
94
|
+
}
|
|
95
|
+
async function newest(ids, dir) {
|
|
96
|
+
let best;
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
try {
|
|
99
|
+
const info = await (0, promises_1.stat)(node_path_1.default.join(dir, id));
|
|
100
|
+
if (!best || info.mtimeMs > best.mtime)
|
|
101
|
+
best = { id, mtime: info.mtimeMs };
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* skip */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return best?.id;
|
|
108
|
+
}
|
|
109
|
+
function helpText() {
|
|
110
|
+
return `browzer — agent-browser wrapper that records a debug trace
|
|
111
|
+
|
|
112
|
+
Usage:
|
|
113
|
+
browzer [--output <dir>] <agent-browser args...>
|
|
114
|
+
browzer traces [--output <dir>]
|
|
115
|
+
browzer inspect [id] [--output <dir>]
|
|
116
|
+
browzer replay [id|path] [--output <dir>]
|
|
117
|
+
browzer skills [list|get|path|install] [--project]
|
|
118
|
+
browzer help
|
|
119
|
+
|
|
120
|
+
--output / -o Directory for traces. Default: current working directory.
|
|
121
|
+
Also BROWZER_OUTPUT.
|
|
122
|
+
|
|
123
|
+
On first successful command, browzer starts video recording and HAR capture.
|
|
124
|
+
On close, it dumps console, errors, a snapshot, and a poster.
|
|
125
|
+
|
|
126
|
+
Use agent-browser dashboard for live observation. Browzer is the saved bundle.
|
|
127
|
+
`;
|
|
128
|
+
}
|