@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,45 @@
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.packageRoot = packageRoot;
7
+ exports.browzerHome = browzerHome;
8
+ exports.tracesDir = tracesDir;
9
+ exports.activeDir = activeDir;
10
+ exports.activePath = activePath;
11
+ exports.traceDir = traceDir;
12
+ exports.sanitize = sanitize;
13
+ exports.newTraceId = newTraceId;
14
+ const node_os_1 = require("node:os");
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ /** Repo or npm package root (parent of `cli/` or `dist/cli/`). */
17
+ function packageRoot() {
18
+ const parent = node_path_1.default.basename(node_path_1.default.dirname(__dirname));
19
+ if (parent === "dist")
20
+ return node_path_1.default.resolve(__dirname, "..", "..");
21
+ return node_path_1.default.resolve(__dirname, "..");
22
+ }
23
+ function browzerHome() {
24
+ return process.env.BROWZER_HOME?.trim() || node_path_1.default.join((0, node_os_1.homedir)(), ".browzer");
25
+ }
26
+ function tracesDir(output) {
27
+ const raw = output?.trim() || process.env.BROWZER_OUTPUT?.trim() || process.cwd();
28
+ return node_path_1.default.resolve(raw);
29
+ }
30
+ function activeDir() {
31
+ return node_path_1.default.join(browzerHome(), "active");
32
+ }
33
+ function activePath(session) {
34
+ return node_path_1.default.join(activeDir(), `${sanitize(session)}.json`);
35
+ }
36
+ function traceDir(id, output) {
37
+ return node_path_1.default.join(tracesDir(output), id);
38
+ }
39
+ function sanitize(value) {
40
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
41
+ }
42
+ function newTraceId(session) {
43
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
44
+ return `${stamp}-${sanitize(session)}`;
45
+ }
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderReplayHtml = renderReplayHtml;
4
+ function renderReplayHtml(trace) {
5
+ const payload = {
6
+ id: trace.id,
7
+ session: trace.session,
8
+ title: trace.title || trace.session,
9
+ url: trace.lastUrl || trace.startUrl || "",
10
+ startedAt: trace.startedAt,
11
+ endedAt: trace.endedAt ?? null,
12
+ hasVideo: trace.hasVideo,
13
+ hasPoster: trace.hasPoster,
14
+ commands: trace.commands.map((cmd) => ({
15
+ id: cmd.id,
16
+ ts: cmd.ts,
17
+ label: cmd.label,
18
+ durationMs: cmd.durationMs,
19
+ exitCode: cmd.exitCode,
20
+ })),
21
+ console: trace.console,
22
+ errors: trace.errors,
23
+ network: trace.network,
24
+ snapshot: trace.snapshot,
25
+ urls: trace.urls,
26
+ };
27
+ const json = JSON.stringify(payload).replace(/</g, "\\u003c");
28
+ return `<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="utf-8" />
32
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
33
+ <title>Browzer · ${escapeHtml(payload.title)}</title>
34
+ <style>
35
+ :root {
36
+ --bg: oklch(0.155 0.02 250);
37
+ --raise: oklch(0.2 0.022 250);
38
+ --ink: oklch(0.94 0.012 250);
39
+ --muted: oklch(0.72 0.025 250);
40
+ --faint: oklch(0.55 0.02 250);
41
+ --line: oklch(0.3 0.02 250);
42
+ --panel: oklch(0.965 0.014 155);
43
+ --panel-ink: oklch(0.22 0.04 155);
44
+ --panel-muted: oklch(0.42 0.045 155);
45
+ --panel-line: oklch(0.88 0.02 155);
46
+ --panel-fill: oklch(0.94 0.018 155);
47
+ --live: oklch(0.42 0.12 155);
48
+ --live-bright: oklch(0.78 0.15 155);
49
+ --danger: oklch(0.63 0.19 25);
50
+ --chrome: oklch(0.22 0.012 250);
51
+ --chrome-ink: oklch(0.86 0.01 250);
52
+ }
53
+ * { box-sizing: border-box; }
54
+ html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--ink); }
55
+ body { font-family: ui-sans-serif, system-ui, sans-serif; letter-spacing: -0.011em; }
56
+ button { font: inherit; }
57
+ code, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
58
+ .app { display: flex; flex-direction: column; gap: 16px; padding: 16px; height: 100dvh; overflow: hidden; }
59
+ .row { display: flex; flex-direction: column; gap: 16px; flex: 1; min-height: 0; }
60
+ .main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; }
61
+ @media (min-width: 960px) {
62
+ .row { flex-direction: row; }
63
+ }
64
+ .top { display: flex; justify-content: space-between; padding: 0 4px; font-size: 12px; }
65
+ .kicker { text-transform: uppercase; letter-spacing: 0.16em; color: var(--muted); }
66
+ .meta { color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
67
+ .frame { flex: 1; min-height: 240px; display: flex; flex-direction: column; overflow: hidden; border-radius: 12px; background: var(--chrome); color: var(--chrome-ink); }
68
+ .chrome { display: flex; align-items: center; gap: 12px; padding: 10px 12px; }
69
+ .dots { display: flex; gap: 6px; padding-left: 4px; }
70
+ .dots span { width: 10px; height: 10px; border-radius: 50%; }
71
+ .url { flex: 1; min-width: 0; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: transparent; padding: 4px 0; font-size: 12px; }
72
+ .stage { position: relative; flex: 1; min-height: 0; background: #000; }
73
+ .stage video, .stage img { width: 100%; height: 100%; object-fit: contain; }
74
+ .empty { display: flex; height: 100%; align-items: center; justify-content: center; font-size: 12px; color: var(--faint); }
75
+ .rail { width: 100%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--panel); color: var(--panel-ink); border-radius: 12px; }
76
+ @media (min-width: 960px) { .rail { width: 300px; flex-shrink: 0; } }
77
+ @media (max-width: 959px) { .rail { max-height: 42dvh; } }
78
+ .rail-head { flex-shrink: 0; padding: 20px 24px 8px; }
79
+ .replay-kicker { font-size: 11px; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--live); }
80
+ .timer { margin: 8px 0 0; font-family: ui-monospace, Menlo, monospace; font-size: clamp(2.75rem, 5vw, 4.25rem); font-weight: 500; letter-spacing: -0.03em; line-height: 0.9; font-variant-numeric: tabular-nums; }
81
+ .secs { margin: 6px 0 0; font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--panel-muted); }
82
+ .steps { flex: 1; min-height: 0; overflow-y: auto; padding: 0 20px 16px; margin: 0; list-style: none; }
83
+ .step { width: 100%; display: flex; gap: 12px; padding: 10px 8px; border: 0; background: transparent; text-align: left; border-radius: 6px; cursor: pointer; color: inherit; }
84
+ .step:hover, .step.current { background: var(--panel-fill); }
85
+ .dot { width: 14px; height: 14px; margin-top: 6px; flex-shrink: 0; border-radius: 50%; border: 2px solid var(--panel-line); }
86
+ .dot.done { background: var(--live); border-color: var(--live); }
87
+ .dot.error { background: var(--danger); border-color: var(--danger); }
88
+ .dot.running { border-color: var(--live); background: color-mix(in oklch, var(--live) 20%, white); box-shadow: 0 0 0 4px color-mix(in oklch, var(--live) 18%, transparent); }
89
+ .label { flex: 1; min-width: 0; font-size: 14px; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
90
+ .label.pending { color: var(--panel-muted); }
91
+ .t { flex-shrink: 0; font-size: 11px; font-variant-numeric: tabular-nums; color: var(--panel-muted); }
92
+ .rail-foot { flex-shrink: 0; margin-top: auto; border-top: 1px solid var(--panel-line); padding: 12px 24px 16px; }
93
+ .count { margin: 0; font-size: 22px; font-weight: 500; letter-spacing: -0.02em; }
94
+ .hint { margin: 4px 0 0; font-size: 13px; color: var(--panel-muted); }
95
+ .detail-bar { flex: 0 0 auto; align-self: flex-start; border: 0; border-radius: 8px; background: var(--raise); color: var(--muted); padding: 8px 12px; cursor: pointer; font-size: 13px; }
96
+ .detail-bar:hover { color: var(--ink); }
97
+ .details { display: flex; flex-direction: column; flex: 0 0 240px; height: 240px; min-height: 240px; max-height: 240px; overflow: hidden; border-radius: 12px; background: var(--raise); }
98
+ .details[hidden] { display: none !important; }
99
+ .tabs { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; border-bottom: 1px solid var(--line); padding: 8px 12px 0; }
100
+ .tabs button { border: 0; background: transparent; color: var(--muted); padding: 8px 12px; border-radius: 6px 6px 0 0; cursor: pointer; font-size: 13px; }
101
+ .tabs button.on { background: var(--bg); color: var(--ink); }
102
+ .tabs .hide { margin-left: auto; }
103
+ .panel { flex: 1; min-height: 0; overflow: auto; padding: 12px 16px; font-size: 12px; line-height: 1.55; font-family: ui-monospace, Menlo, monospace; }
104
+ .err { color: var(--danger); }
105
+ .muted { color: var(--muted); }
106
+ .faint { color: var(--faint); }
107
+ .net { display: grid; grid-template-columns: 2.5em 3.5em 1fr; gap: 8px; }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <div class="app">
112
+ <div class="row">
113
+ <div class="main">
114
+ <div class="top">
115
+ <span class="kicker mono">Browzer</span>
116
+ <span class="meta mono" id="heading"></span>
117
+ </div>
118
+ <section class="frame">
119
+ <header class="chrome">
120
+ <div class="dots" aria-hidden="true"><span style="background:#ff5f57"></span><span style="background:#febc2e"></span><span style="background:#28c840"></span></div>
121
+ <p class="url mono" id="url"></p>
122
+ </header>
123
+ <div class="stage" id="stage"></div>
124
+ </section>
125
+ <button type="button" class="detail-bar" id="show-details">Show details</button>
126
+ <section class="details" id="details" hidden>
127
+ <div class="tabs" id="tabs"></div>
128
+ <div class="panel" id="panel"></div>
129
+ </section>
130
+ </div>
131
+ <aside class="rail">
132
+ <div class="rail-head">
133
+ <p class="replay-kicker mono">Replay</p>
134
+ <p class="timer" id="timer">0.00</p>
135
+ <p class="secs mono">Seconds elapsed</p>
136
+ </div>
137
+ <ol class="steps" id="steps"></ol>
138
+ <div class="rail-foot">
139
+ <p class="count" id="count">0 actions executed</p>
140
+ <p class="hint" id="median"></p>
141
+ </div>
142
+ </aside>
143
+ </div>
144
+ </div>
145
+ <script id="data" type="application/json">${json}</script>
146
+ <script>
147
+ const T = JSON.parse(document.getElementById("data").textContent);
148
+ document.getElementById("heading").textContent = T.title + " · " + T.id;
149
+ document.getElementById("url").textContent = (T.urls && T.urls[0] && T.urls[0].url) || T.url || "about:blank";
150
+ const stage = document.getElementById("stage");
151
+ let video = null;
152
+ if (T.hasVideo) {
153
+ video = document.createElement("video");
154
+ video.controls = true;
155
+ video.playsInline = true;
156
+ video.src = "session.mp4";
157
+ if (T.hasPoster) video.poster = "poster.jpg";
158
+ stage.appendChild(video);
159
+ } else if (T.hasPoster) {
160
+ const img = document.createElement("img");
161
+ img.src = "poster.jpg";
162
+ img.alt = "Last captured frame";
163
+ stage.appendChild(img);
164
+ } else {
165
+ stage.innerHTML = '<div class="empty mono">No recording was saved</div>';
166
+ }
167
+
168
+ const origin = (T.commands[0] && T.commands[0].ts) || T.startedAt;
169
+ function scaleFor(duration) {
170
+ const maxT = T.commands.reduce((m, c) => Math.max(m, Math.max(0, (c.ts - origin) / 1000)), 0);
171
+ return duration > 0 && maxT > duration + 0.25 ? Math.max(0, duration - 0.08) / maxT : 1;
172
+ }
173
+ function cueList(duration) {
174
+ const scale = scaleFor(duration);
175
+ return T.commands.map((c) => ({ id: c.id, seconds: Math.max(0, (c.ts - origin) / 1000) * scale }));
176
+ }
177
+ function urlAt(t, duration) {
178
+ const scale = scaleFor(duration);
179
+ let url = (T.urls && T.urls[0] && T.urls[0].url) || T.url || "about:blank";
180
+ for (const point of T.urls || []) {
181
+ const seconds = Math.max(0, (point.ts - origin) / 1000) * scale;
182
+ if (seconds <= t + 0.04) url = point.url;
183
+ }
184
+ return url;
185
+ }
186
+ function visual(id, t, duration, cues) {
187
+ const cue = cues.find((c) => c.id === id);
188
+ if (!cue) return "pending";
189
+ if (duration > 0 && t >= Math.max(0, duration - 0.08)) return "done";
190
+ if (t + 0.04 < cue.seconds) return "pending";
191
+ let active = null;
192
+ for (const c of cues) if (c.seconds <= t + 0.04) active = c.id;
193
+ return active === id ? "running" : "done";
194
+ }
195
+ function clock(s) {
196
+ const m = Math.floor(Math.max(0, s) / 60);
197
+ const sec = Math.floor(Math.max(0, s) % 60);
198
+ return m + ":" + String(sec).padStart(2, "0");
199
+ }
200
+ const stepsEl = document.getElementById("steps");
201
+ T.commands.forEach((cmd) => {
202
+ const li = document.createElement("li");
203
+ li.innerHTML = '<button class="step" type="button"><span class="dot"></span><span class="label"></span><span class="t mono"></span></button>';
204
+ li.querySelector(".label").textContent = cmd.label;
205
+ li.querySelector("button").addEventListener("click", () => {
206
+ const cues = cueList(video && video.duration || 0);
207
+ const cue = cues.find((c) => c.id === cmd.id);
208
+ if (video && cue) { video.currentTime = cue.seconds; video.play(); }
209
+ });
210
+ stepsEl.appendChild(li);
211
+ });
212
+ function render(t) {
213
+ const duration = video && isFinite(video.duration) ? video.duration : 0;
214
+ const cues = cueList(duration);
215
+ document.getElementById("timer").textContent = t.toFixed(2);
216
+ document.getElementById("url").textContent = urlAt(t, duration);
217
+ let done = 0;
218
+ [...stepsEl.children].forEach((li, i) => {
219
+ const cmd = T.commands[i];
220
+ const cue = cues[i];
221
+ const state = visual(cmd.id, t, duration, cues);
222
+ if (state === "done") done += 1;
223
+ const dot = li.querySelector(".dot");
224
+ const label = li.querySelector(".label");
225
+ const btn = li.querySelector("button");
226
+ dot.className = "dot " + (cmd.exitCode && state === "done" ? "error" : state);
227
+ label.className = "label" + (state === "pending" ? " pending" : "");
228
+ btn.className = "step" + (state === "running" ? " current" : "");
229
+ li.querySelector(".t").textContent = cue ? clock(cue.seconds) : "";
230
+ });
231
+ document.getElementById("count").textContent = done + (done === 1 ? " action executed" : " actions executed");
232
+ const durs = T.commands.map((c) => c.durationMs).filter((n) => n > 0).sort((a,b) => a-b);
233
+ let med = "";
234
+ if (durs.length) {
235
+ const mid = Math.floor(durs.length / 2);
236
+ const ms = durs.length % 2 ? durs[mid] : (durs[mid-1] + durs[mid]) / 2;
237
+ med = (ms < 1000 ? Math.round(ms) + "ms" : (ms/1000).toFixed(1) + "s") + " median step";
238
+ }
239
+ document.getElementById("median").textContent = med;
240
+ }
241
+ if (video) {
242
+ video.addEventListener("timeupdate", () => render(video.currentTime));
243
+ video.addEventListener("loadedmetadata", () => render(video.currentTime || 0));
244
+ video.addEventListener("seeked", () => render(video.currentTime));
245
+ video.addEventListener("play", () => {
246
+ const tick = () => { if (!video.paused && !video.ended) { render(video.currentTime); requestAnimationFrame(tick); } };
247
+ requestAnimationFrame(tick);
248
+ });
249
+ }
250
+ render(0);
251
+
252
+ const details = document.getElementById("details");
253
+ const showBtn = document.getElementById("show-details");
254
+ function setDetails(open) {
255
+ details.hidden = !open;
256
+ showBtn.hidden = open;
257
+ }
258
+ showBtn.addEventListener("click", () => setDetails(true));
259
+ setDetails(false);
260
+
261
+ const tabs = ["Console", "Network", "Errors", "Snapshot"];
262
+ const tabsEl = document.getElementById("tabs");
263
+ const panel = document.getElementById("panel");
264
+ let on = "Console";
265
+ function paint() {
266
+ [...tabsEl.children].forEach((b) => b.classList.toggle("on", b.dataset.tab === on));
267
+ if (on === "Console") {
268
+ panel.innerHTML = T.console.length ? T.console.map((m) =>
269
+ '<div class="' + (m.type === "error" ? "err" : "muted") + '"><span class="faint">' + esc(m.type).toUpperCase() + "</span> " + esc(m.text) + "</div>"
270
+ ).join("") : '<p class="faint">No console output.</p>';
271
+ } else if (on === "Network") {
272
+ panel.innerHTML = T.network.length ? T.network.map((r) =>
273
+ '<div class="net ' + (r.status >= 400 ? "err" : "muted") + '"><span>' + (r.status || "—") + "</span><span class='faint'>" + esc(r.method) + "</span><span>" + esc(r.url) + "</span></div>"
274
+ ).join("") : '<p class="faint">No HAR was saved.</p>';
275
+ } else if (on === "Errors") {
276
+ const items = T.errors.map((e) => e.text).concat(T.console.filter((m) => m.type === "error").map((m) => m.text));
277
+ panel.innerHTML = items.length ? items.map((t) => '<div class="err">' + esc(t) + "</div>").join("") : '<p class="faint">No page or console errors.</p>';
278
+ } else {
279
+ panel.innerHTML = T.snapshot ? "<pre class='muted' style='white-space:pre-wrap;margin:0'>" + esc(T.snapshot) + "</pre>" : '<p class="faint">No snapshot was saved.</p>';
280
+ }
281
+ }
282
+ function esc(s) {
283
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
284
+ }
285
+ tabs.forEach((name) => {
286
+ const b = document.createElement("button");
287
+ b.type = "button";
288
+ b.dataset.tab = name;
289
+ const extra = name === "Console" ? T.console.length : name === "Network" ? T.network.length : name === "Errors" ? (T.errors.length + T.console.filter((m)=>m.type==="error").length) : "";
290
+ b.textContent = extra === "" ? name : name + " " + extra;
291
+ b.addEventListener("click", () => { on = name; paint(); });
292
+ tabsEl.appendChild(b);
293
+ });
294
+ const hide = document.createElement("button");
295
+ hide.type = "button";
296
+ hide.className = "hide";
297
+ hide.textContent = "Hide";
298
+ hide.addEventListener("click", () => setDetails(false));
299
+ tabsEl.appendChild(hide);
300
+ paint();
301
+ </script>
302
+ </body>
303
+ </html>
304
+ `;
305
+ }
306
+ function escapeHtml(value) {
307
+ return value.replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch] ?? ch);
308
+ }
@@ -0,0 +1,89 @@
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.replayCommand = replayCommand;
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
+ const node_child_process_1 = require("node:child_process");
11
+ const paths_1 = require("./paths");
12
+ const trace_store_1 = require("../lib/trace-store");
13
+ const replay_html_1 = require("./replay-html");
14
+ async function replayCommand(arg, output) {
15
+ const dir = await resolveTraceDir(arg, output);
16
+ const trace = await (0, trace_store_1.getTraceFromDir)(dir);
17
+ if (!trace) {
18
+ process.stderr.write(`No trace found at ${dir}\n`);
19
+ process.exitCode = 1;
20
+ return;
21
+ }
22
+ const htmlPath = node_path_1.default.join(dir, "replay.html");
23
+ await (0, promises_1.writeFile)(htmlPath, (0, replay_html_1.renderReplayHtml)(trace));
24
+ openPath(htmlPath);
25
+ process.stdout.write(`opened ${htmlPath}\n`);
26
+ }
27
+ async function resolveTraceDir(arg, output) {
28
+ const root = (0, paths_1.tracesDir)(output);
29
+ if (arg) {
30
+ const resolved = node_path_1.default.resolve(arg);
31
+ if ((0, node_fs_1.existsSync)(resolved)) {
32
+ if ((0, node_fs_1.statSync)(resolved).isDirectory())
33
+ return resolved;
34
+ return node_path_1.default.dirname(resolved);
35
+ }
36
+ const id = await matchId(arg, root);
37
+ if (id)
38
+ return node_path_1.default.join(root, id);
39
+ throw new Error(`Trace not found: ${arg}`);
40
+ }
41
+ const latest = await latestId(root);
42
+ if (!latest)
43
+ throw new Error("No traces yet. Record one with browzer <agent-browser command>.");
44
+ return node_path_1.default.join(root, latest);
45
+ }
46
+ async function latestId(dir) {
47
+ if (!(0, node_fs_1.existsSync)(dir))
48
+ return null;
49
+ let best = null;
50
+ for (const id of await (0, promises_1.readdir)(dir)) {
51
+ try {
52
+ const info = await (0, promises_1.stat)(node_path_1.default.join(dir, id));
53
+ if (!best || info.mtimeMs > best.mtime)
54
+ best = { id, mtime: info.mtimeMs };
55
+ }
56
+ catch {
57
+ /* skip */
58
+ }
59
+ }
60
+ return best?.id ?? null;
61
+ }
62
+ async function matchId(prefix, dir) {
63
+ if (!(0, node_fs_1.existsSync)(dir))
64
+ return null;
65
+ const ids = (await (0, promises_1.readdir)(dir)).filter((id) => id === prefix || id.startsWith(prefix) || id.endsWith(`-${prefix}`));
66
+ if (ids.length === 0)
67
+ return null;
68
+ if (ids.length === 1)
69
+ return ids[0];
70
+ let best = null;
71
+ for (const id of ids) {
72
+ const info = await (0, promises_1.stat)(node_path_1.default.join(dir, id));
73
+ if (!best || info.mtimeMs > best.mtime)
74
+ best = { id, mtime: info.mtimeMs };
75
+ }
76
+ return best?.id ?? null;
77
+ }
78
+ function openPath(file) {
79
+ const platform = process.platform;
80
+ if (platform === "darwin") {
81
+ (0, node_child_process_1.spawn)("open", [file], { detached: true, stdio: "ignore" }).unref();
82
+ return;
83
+ }
84
+ if (platform === "win32") {
85
+ (0, node_child_process_1.spawn)("cmd", ["/c", "start", "", file], { detached: true, stdio: "ignore" }).unref();
86
+ return;
87
+ }
88
+ (0, node_child_process_1.spawn)("xdg-open", [file], { detached: true, stdio: "ignore" }).unref();
89
+ }
@@ -0,0 +1,106 @@
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.skillSourceDir = skillSourceDir;
7
+ exports.skillsCommand = skillsCommand;
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_fs_1 = require("node:fs");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const paths_1 = require("./paths");
13
+ const SKILL_NAME = "browzer";
14
+ const TARGETS = [
15
+ { name: "agents", global: node_path_1.default.join((0, node_os_1.homedir)(), ".agents", "skills"), project: ".agents/skills", always: true },
16
+ { name: "claude", global: node_path_1.default.join((0, node_os_1.homedir)(), ".claude", "skills"), project: ".claude/skills" },
17
+ { name: "cursor", global: node_path_1.default.join((0, node_os_1.homedir)(), ".cursor", "skills"), project: ".cursor/skills" },
18
+ { name: "codex", global: node_path_1.default.join((0, node_os_1.homedir)(), ".codex", "skills"), project: ".codex/skills" },
19
+ { name: "grok", global: node_path_1.default.join((0, node_os_1.homedir)(), ".grok", "skills"), project: ".grok/skills" },
20
+ ];
21
+ function skillSourceDir() {
22
+ return node_path_1.default.join((0, paths_1.packageRoot)(), "skills", SKILL_NAME);
23
+ }
24
+ async function skillsCommand(argv, json) {
25
+ const rest = argv.slice(argv.indexOf("skills") + 1).filter((token) => !token.startsWith("-"));
26
+ const sub = rest[0] ?? "list";
27
+ if (sub === "list") {
28
+ await listSkills(json);
29
+ return;
30
+ }
31
+ if (sub === "get") {
32
+ await getSkill();
33
+ return;
34
+ }
35
+ if (sub === "path") {
36
+ process.stdout.write(`${skillSourceDir()}\n`);
37
+ return;
38
+ }
39
+ if (sub === "install") {
40
+ const project = argv.includes("--project");
41
+ await installSkill(project);
42
+ return;
43
+ }
44
+ process.stderr.write("Usage: browzer skills [list|get|path|install] [--project]\n");
45
+ process.exitCode = 1;
46
+ }
47
+ async function listSkills(json) {
48
+ const source = skillSourceDir();
49
+ const skill = {
50
+ name: SKILL_NAME,
51
+ path: source,
52
+ description: "agent-browser wrapper that records a replayable trace",
53
+ };
54
+ if (json) {
55
+ process.stdout.write(`${JSON.stringify({ skills: [skill] }, null, 2)}\n`);
56
+ return;
57
+ }
58
+ process.stdout.write(`${skill.name} ${skill.path}\n`);
59
+ }
60
+ async function getSkill() {
61
+ const file = node_path_1.default.join(skillSourceDir(), "SKILL.md");
62
+ if (!(0, node_fs_1.existsSync)(file)) {
63
+ process.stderr.write(`Skill not found at ${file}\n`);
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ process.stdout.write(await (0, promises_1.readFile)(file, "utf8"));
68
+ }
69
+ async function installSkill(project) {
70
+ const source = node_path_1.default.join(skillSourceDir(), "SKILL.md");
71
+ if (!(0, node_fs_1.existsSync)(source)) {
72
+ process.stderr.write(`Skill not found at ${source}\n`);
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+ const cwd = process.cwd();
77
+ const written = [];
78
+ for (const target of TARGETS) {
79
+ const root = project ? node_path_1.default.resolve(cwd, target.project) : target.global;
80
+ const marker = project
81
+ ? node_path_1.default.dirname(node_path_1.default.resolve(cwd, target.project))
82
+ : node_path_1.default.dirname(target.global);
83
+ if (!target.always && !(0, node_fs_1.existsSync)(marker) && !(0, node_fs_1.existsSync)(root))
84
+ continue;
85
+ const destDir = node_path_1.default.join(root, SKILL_NAME);
86
+ try {
87
+ const st = await (0, promises_1.lstat)(destDir);
88
+ if (!st.isDirectory() && !st.isSymbolicLink())
89
+ await (0, promises_1.mkdir)(destDir, { recursive: true });
90
+ }
91
+ catch {
92
+ await (0, promises_1.mkdir)(destDir, { recursive: true });
93
+ }
94
+ const dest = node_path_1.default.join(destDir, "SKILL.md");
95
+ await (0, promises_1.copyFile)(source, dest);
96
+ written.push(dest);
97
+ }
98
+ if (written.length === 0) {
99
+ process.stderr.write("No agent skill directories found. Pass --project from a repo, or create ~/.agents/skills.\n");
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ process.stdout.write(`Installed ${SKILL_NAME} to:\n`);
104
+ for (const dest of written)
105
+ process.stdout.write(` ${dest}\n`);
106
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadActive = loadActive;
4
+ exports.saveActive = saveActive;
5
+ exports.clearActive = clearActive;
6
+ exports.listActive = listActive;
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_fs_1 = require("node:fs");
9
+ const paths_1 = require("./paths");
10
+ async function loadActive(session) {
11
+ const file = (0, paths_1.activePath)(session);
12
+ if (!(0, node_fs_1.existsSync)(file))
13
+ return null;
14
+ try {
15
+ const data = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
16
+ if (!data.dir || !(0, node_fs_1.existsSync)(data.dir)) {
17
+ await clearActive(session);
18
+ return null;
19
+ }
20
+ return data;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ async function saveActive(state) {
27
+ await (0, promises_1.mkdir)((0, paths_1.activeDir)(), { recursive: true });
28
+ await (0, promises_1.writeFile)((0, paths_1.activePath)(state.session), JSON.stringify(state, null, 2));
29
+ }
30
+ async function clearActive(session) {
31
+ await (0, promises_1.rm)((0, paths_1.activePath)(session), { force: true });
32
+ }
33
+ async function listActive() {
34
+ if (!(0, node_fs_1.existsSync)((0, paths_1.activeDir)()))
35
+ return [];
36
+ const names = await (0, promises_1.readdir)((0, paths_1.activeDir)());
37
+ const out = [];
38
+ for (const name of names) {
39
+ if (!name.endsWith(".json"))
40
+ continue;
41
+ const session = name.slice(0, -".json".length);
42
+ const active = await loadActive(session);
43
+ if (active)
44
+ out.push(active);
45
+ }
46
+ return out;
47
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectDuring = collectDuring;
4
+ async function collectDuring(port, fn, onEvent) {
5
+ if (!port)
6
+ return fn();
7
+ const events = [];
8
+ let ws = null;
9
+ try {
10
+ ws = await connect(port);
11
+ ws.addEventListener("message", (ev) => {
12
+ try {
13
+ const msg = JSON.parse(String(ev.data));
14
+ if (msg.type === "frame")
15
+ return;
16
+ events.push(msg);
17
+ onEvent?.(msg);
18
+ }
19
+ catch {
20
+ /* ignore */
21
+ }
22
+ });
23
+ }
24
+ catch {
25
+ ws = null;
26
+ }
27
+ try {
28
+ return await fn();
29
+ }
30
+ finally {
31
+ await wait(120);
32
+ ws?.close();
33
+ void events;
34
+ }
35
+ }
36
+ function connect(port) {
37
+ return new Promise((resolve, reject) => {
38
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/`);
39
+ const timer = setTimeout(() => {
40
+ ws.close();
41
+ reject(new Error("stream connect timeout"));
42
+ }, 1500);
43
+ ws.addEventListener("open", () => {
44
+ clearTimeout(timer);
45
+ resolve(ws);
46
+ });
47
+ ws.addEventListener("error", () => {
48
+ clearTimeout(timer);
49
+ reject(new Error("stream connect failed"));
50
+ });
51
+ });
52
+ }
53
+ function wait(ms) {
54
+ return new Promise((resolve) => setTimeout(resolve, ms));
55
+ }