@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.
@@ -0,0 +1,116 @@
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.writeSummary = writeSummary;
7
+ const node_fs_1 = require("node:fs");
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ async function writeSummary(trace, extras) {
11
+ const commands = await readJsonl(node_path_1.default.join(trace.dir, "commands.jsonl"));
12
+ const failed = commands.filter((row) => row.exitCode !== 0);
13
+ const errorsRaw = await readOptional(node_path_1.default.join(trace.dir, "errors.json"));
14
+ const consoleRaw = await readOptional(node_path_1.default.join(trace.dir, "console.json"));
15
+ const harIssues = await harFailures(node_path_1.default.join(trace.dir, "network.har"));
16
+ const errorCount = countErrors(errorsRaw);
17
+ const consoleErrors = countConsoleErrors(consoleRaw);
18
+ const endedAt = Date.now();
19
+ const durationSec = ((endedAt - trace.startedAt) / 1000).toFixed(1);
20
+ const lines = [
21
+ `# ${trace.id}`,
22
+ "",
23
+ `- session: \`${trace.session}\``,
24
+ `- duration: ${durationSec}s`,
25
+ extras.url ? `- url: ${extras.url}` : null,
26
+ extras.title ? `- title: ${extras.title}` : null,
27
+ `- commands: ${commands.length} (${failed.length} failed)`,
28
+ `- page errors: ${errorCount}`,
29
+ `- console errors: ${consoleErrors}`,
30
+ `- har failures: ${harIssues.length}`,
31
+ "",
32
+ "## Failed commands",
33
+ failed.length === 0
34
+ ? "_none_"
35
+ : failed
36
+ .map((row) => `- \`${row.argv.join(" ")}\` (exit ${row.exitCode})`)
37
+ .join("\n"),
38
+ "",
39
+ "## Network failures",
40
+ harIssues.length === 0 ? "_none_" : harIssues.map((item) => `- ${item}`).join("\n"),
41
+ "",
42
+ "## Files",
43
+ ...[
44
+ "session.mp4",
45
+ "network.har",
46
+ "commands.jsonl",
47
+ "events.jsonl",
48
+ "console.json",
49
+ "errors.json",
50
+ "snapshot.txt",
51
+ "poster.jpg",
52
+ ]
53
+ .filter((name) => (0, node_fs_1.existsSync)(node_path_1.default.join(trace.dir, name)))
54
+ .map((name) => `- ${name}`),
55
+ "",
56
+ ].filter((line) => line != null);
57
+ await (0, promises_1.writeFile)(node_path_1.default.join(trace.dir, "summary.md"), `${lines.join("\n")}\n`);
58
+ }
59
+ async function readJsonl(file) {
60
+ if (!(0, node_fs_1.existsSync)(file))
61
+ return [];
62
+ const text = await (0, promises_1.readFile)(file, "utf8");
63
+ return text
64
+ .split("\n")
65
+ .map((line) => line.trim())
66
+ .filter(Boolean)
67
+ .flatMap((line) => {
68
+ try {
69
+ return [JSON.parse(line)];
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ });
75
+ }
76
+ async function readOptional(file) {
77
+ if (!(0, node_fs_1.existsSync)(file))
78
+ return null;
79
+ try {
80
+ return JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ function countErrors(raw) {
87
+ if (!raw || typeof raw !== "object")
88
+ return 0;
89
+ const data = raw.data ?? raw;
90
+ const errors = data.errors;
91
+ return Array.isArray(errors) ? errors.length : 0;
92
+ }
93
+ function countConsoleErrors(raw) {
94
+ if (!raw || typeof raw !== "object")
95
+ return 0;
96
+ const data = raw.data ?? raw;
97
+ const messages = data.messages;
98
+ if (!Array.isArray(messages))
99
+ return 0;
100
+ return messages.filter((msg) => msg.type === "error" || msg.type === "warning").length;
101
+ }
102
+ async function harFailures(file) {
103
+ if (!(0, node_fs_1.existsSync)(file))
104
+ return [];
105
+ try {
106
+ const har = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
107
+ const entries = har.log?.entries ?? [];
108
+ return entries
109
+ .filter((entry) => (entry.response?.status ?? 0) >= 400)
110
+ .slice(0, 20)
111
+ .map((entry) => `${entry.response?.status} ${entry.request?.method} ${entry.request?.url}`);
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ }
@@ -0,0 +1,227 @@
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.assertTraceId = assertTraceId;
7
+ exports.commandLabel = commandLabel;
8
+ exports.listTraces = listTraces;
9
+ exports.getTrace = getTrace;
10
+ exports.getTraceFromDir = getTraceFromDir;
11
+ exports.traceFile = traceFile;
12
+ const node_fs_1 = require("node:fs");
13
+ const promises_1 = require("node:fs/promises");
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const paths_1 = require("../cli/paths");
16
+ const ID_RE = /^[a-zA-Z0-9._-]+$/;
17
+ function assertTraceId(id) {
18
+ if (!ID_RE.test(id))
19
+ throw new Error("Invalid trace id");
20
+ }
21
+ function commandLabel(argv) {
22
+ const out = [];
23
+ for (let i = 0; i < argv.length; i += 1) {
24
+ const token = argv[i] ?? "";
25
+ if (token === "--session" || token === "--headed" || token === "--idle-timeout") {
26
+ i += 1;
27
+ continue;
28
+ }
29
+ if (token.startsWith("--session=") || token === "--json")
30
+ continue;
31
+ out.push(token);
32
+ }
33
+ return out.join(" ") || argv.join(" ");
34
+ }
35
+ async function listTraces() {
36
+ const root = (0, paths_1.tracesDir)();
37
+ if (!(0, node_fs_1.existsSync)(root))
38
+ return [];
39
+ const names = await (0, promises_1.readdir)(root);
40
+ const traces = [];
41
+ for (const id of names) {
42
+ if (!ID_RE.test(id))
43
+ continue;
44
+ const meta = await readMeta(id);
45
+ if (meta)
46
+ traces.push(meta);
47
+ }
48
+ traces.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
49
+ return traces;
50
+ }
51
+ async function getTrace(id) {
52
+ assertTraceId(id);
53
+ return getTraceFromDir(node_path_1.default.join((0, paths_1.tracesDir)(), id));
54
+ }
55
+ async function getTraceFromDir(dir) {
56
+ const resolved = node_path_1.default.resolve(dir);
57
+ const id = node_path_1.default.basename(resolved);
58
+ const meta = await readMetaAt(resolved, id);
59
+ if (!meta)
60
+ return null;
61
+ const commands = await readCommands(resolved);
62
+ return {
63
+ ...meta,
64
+ commands,
65
+ console: await readConsole(resolved),
66
+ errors: await readErrors(resolved),
67
+ network: await readNetwork(resolved),
68
+ snapshot: await readText(node_path_1.default.join(resolved, "snapshot.txt")),
69
+ urls: await readUrls(resolved, commands, meta),
70
+ };
71
+ }
72
+ function traceFile(id, name) {
73
+ assertTraceId(id);
74
+ return node_path_1.default.join((0, paths_1.tracesDir)(), id, name);
75
+ }
76
+ async function readMeta(id) {
77
+ return readMetaAt(node_path_1.default.join((0, paths_1.tracesDir)(), id), id);
78
+ }
79
+ async function readMetaAt(dir, id) {
80
+ const file = node_path_1.default.join(dir, "meta.json");
81
+ if (!(0, node_fs_1.existsSync)(file))
82
+ return null;
83
+ try {
84
+ const raw = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
85
+ const info = await (0, promises_1.stat)(dir);
86
+ return {
87
+ id,
88
+ session: raw.session || id,
89
+ startedAt: raw.startedAt || info.mtimeMs,
90
+ endedAt: raw.endedAt,
91
+ startUrl: raw.startUrl,
92
+ lastUrl: raw.lastUrl,
93
+ title: raw.title,
94
+ status: raw.status || "unknown",
95
+ dir,
96
+ hasVideo: (0, node_fs_1.existsSync)(node_path_1.default.join(dir, "session.mp4")),
97
+ hasPoster: (0, node_fs_1.existsSync)(node_path_1.default.join(dir, "poster.jpg")),
98
+ hasHar: (0, node_fs_1.existsSync)(node_path_1.default.join(dir, "network.har")),
99
+ };
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ async function readCommands(dir) {
106
+ const file = node_path_1.default.join(dir, "commands.jsonl");
107
+ if (!(0, node_fs_1.existsSync)(file))
108
+ return [];
109
+ const lines = (await (0, promises_1.readFile)(file, "utf8")).split("\n").filter(Boolean);
110
+ return lines.flatMap((line, index) => {
111
+ try {
112
+ const row = JSON.parse(line);
113
+ return [
114
+ {
115
+ id: `c${index + 1}`,
116
+ ts: row.ts,
117
+ argv: row.argv,
118
+ label: commandLabel(row.argv),
119
+ durationMs: row.durationMs,
120
+ exitCode: row.exitCode,
121
+ stdout: row.stdout || "",
122
+ stderr: row.stderr || "",
123
+ },
124
+ ];
125
+ }
126
+ catch {
127
+ return [];
128
+ }
129
+ });
130
+ }
131
+ async function readConsole(dir) {
132
+ const file = node_path_1.default.join(dir, "console.json");
133
+ if (!(0, node_fs_1.existsSync)(file))
134
+ return [];
135
+ try {
136
+ const raw = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
137
+ return (raw.data?.messages ?? []).map((msg) => ({
138
+ type: msg.type || "log",
139
+ text: msg.text || "",
140
+ timestamp: msg.timestamp,
141
+ }));
142
+ }
143
+ catch {
144
+ return [];
145
+ }
146
+ }
147
+ async function readErrors(dir) {
148
+ const file = node_path_1.default.join(dir, "errors.json");
149
+ if (!(0, node_fs_1.existsSync)(file))
150
+ return [];
151
+ try {
152
+ const raw = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
153
+ return (raw.data?.errors ?? []).map((err) => ({
154
+ text: err.text || err.message || JSON.stringify(err),
155
+ }));
156
+ }
157
+ catch {
158
+ return [];
159
+ }
160
+ }
161
+ async function readNetwork(dir) {
162
+ const file = node_path_1.default.join(dir, "network.har");
163
+ if (!(0, node_fs_1.existsSync)(file))
164
+ return [];
165
+ try {
166
+ const har = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
167
+ return (har.log?.entries ?? []).slice(0, 400).map((entry) => ({
168
+ method: entry.request?.method || "GET",
169
+ url: entry.request?.url || "",
170
+ status: entry.response?.status || 0,
171
+ time: entry.time,
172
+ }));
173
+ }
174
+ catch {
175
+ return [];
176
+ }
177
+ }
178
+ async function readText(file) {
179
+ if (!(0, node_fs_1.existsSync)(file))
180
+ return null;
181
+ return (0, promises_1.readFile)(file, "utf8");
182
+ }
183
+ async function readUrls(dir, commands, meta) {
184
+ const points = [];
185
+ if (meta.startUrl)
186
+ points.push({ ts: meta.startedAt, url: meta.startUrl });
187
+ for (const cmd of commands) {
188
+ const url = openUrlFromArgv(cmd.argv);
189
+ if (url)
190
+ points.push({ ts: cmd.ts, url });
191
+ }
192
+ const eventsFile = node_path_1.default.join(dir, "events.jsonl");
193
+ if ((0, node_fs_1.existsSync)(eventsFile)) {
194
+ const lines = (await (0, promises_1.readFile)(eventsFile, "utf8")).split("\n").filter(Boolean);
195
+ for (const line of lines) {
196
+ try {
197
+ const event = JSON.parse(line);
198
+ const ts = event.timestamp ?? 0;
199
+ if (event.type === "url" && event.url)
200
+ points.push({ ts, url: event.url });
201
+ if (event.type === "tabs") {
202
+ const active = event.tabs?.find((tab) => tab.active) ?? event.tabs?.[0];
203
+ if (active?.url)
204
+ points.push({ ts, url: active.url });
205
+ }
206
+ }
207
+ catch {
208
+ /* skip */
209
+ }
210
+ }
211
+ }
212
+ points.sort((a, b) => a.ts - b.ts);
213
+ const deduped = [];
214
+ for (const point of points) {
215
+ const prev = deduped[deduped.length - 1];
216
+ if (!prev || prev.url !== point.url)
217
+ deduped.push(point);
218
+ }
219
+ return deduped;
220
+ }
221
+ function openUrlFromArgv(argv) {
222
+ const index = argv.findIndex((token) => token === "open" || token === "goto" || token === "navigate");
223
+ if (index < 0)
224
+ return null;
225
+ const url = argv.slice(index + 1).find((token) => !token.startsWith("-"));
226
+ return url || null;
227
+ }
Binary file
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@agentuity/browzer",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "agent-browser wrapper that records a replayable debug trace",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "browzer": "./dist/cli/browzer.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "skills",
16
+ "docs",
17
+ "README.md",
18
+ "LICENSE.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "tsc",
23
+ "browzer": "node dist/cli/browzer.js"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "keywords": [
29
+ "agent-browser",
30
+ "browser",
31
+ "trace",
32
+ "replay",
33
+ "har",
34
+ "agent",
35
+ "cli"
36
+ ],
37
+ "devDependencies": {
38
+ "@types/node": "^20",
39
+ "typescript": "^5"
40
+ }
41
+ }
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: browzer
3
+ description: >
4
+ Record a replayable agent-browser session by invoking the same CLI as
5
+ agent-browser through the browzer wrapper. Use instead of agent-browser when
6
+ the user wants a trace, recording, HAR, console dump, replay, inspect, or
7
+ post-mortem of a browser run. Triggers: browzer, replay the session, record
8
+ this browser run, debug the agent browser session, trace, inspect the
9
+ recording. Use when the user runs /browzer.
10
+ allowed-tools: Bash(browzer:*), Bash(./bin/browzer:*)
11
+ ---
12
+
13
+ # Browzer
14
+
15
+ `browzer` is `agent-browser` with a trace. Same argv, same snapshot/click loop.
16
+
17
+ The command is `browzer` on PATH. In the browzer git checkout, use `./bin/browzer` if PATH is not set. Refresh this skill from the installed CLI with `browzer skills get` so instructions match the binary.
18
+
19
+ Do not run `agent-browser` in the same `--session`. Live watching is `agent-browser dashboard`, not Browzer. For how to snapshot, click, fill, and wait, follow the agent-browser core skill; every process you spawn is `browzer`.
20
+
21
+ ## Session
22
+
23
+ Before the first command:
24
+
25
+ ```bash
26
+ export AGENT_BROWSER_SESSION="$(browzer session id --scope worktree --prefix task)"
27
+ export AGENT_BROWSER_IDLE_TIMEOUT_MS=0
28
+ ```
29
+
30
+ Then pass `--session "$AGENT_BROWSER_SESSION"` on every `browzer` command, or rely on the env var. Pass `--output <dir>` (or `BROWZER_OUTPUT`) to choose where the trace folder is written; default is the current working directory.
31
+
32
+ ## Drive the browser
33
+
34
+ ```bash
35
+ browzer --session "$AGENT_BROWSER_SESSION" open https://example.com
36
+ browzer --session "$AGENT_BROWSER_SESSION" snapshot -i
37
+ browzer --session "$AGENT_BROWSER_SESSION" click @e1
38
+ browzer --session "$AGENT_BROWSER_SESSION" close
39
+ ```
40
+
41
+ `close` finalizes the trace (video, HAR, console, errors, snapshot, `summary.md`). If inspect is empty, the session was never closed.
42
+
43
+ `dashboard`, `mcp`, `doctor`, `install`, and `upgrade` pass through and do not create a trace.
44
+
45
+ ## After the run
46
+
47
+ - Agent: `browzer inspect [id]` (latest if omitted). Read `summary.md` first, then HAR/console/errors in the trace directory (`./<id>/` or `--output`).
48
+ - Human: `browzer replay [id|path]`.
49
+ - List: `browzer traces`.