@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 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
+ ![Replay of a 31-step session](docs/replay.png)
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
+ }
@@ -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
+ }