@hasna/browser 0.4.8 → 0.4.10

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.
@@ -9871,40 +9871,6 @@ var init_schema = __esm(() => {
9871
9871
  init_dist();
9872
9872
  });
9873
9873
 
9874
- // src/db/console-log.ts
9875
- var exports_console_log = {};
9876
- __export(exports_console_log, {
9877
- logConsoleMessage: () => logConsoleMessage,
9878
- getConsoleMessage: () => getConsoleMessage,
9879
- getConsoleLog: () => getConsoleLog,
9880
- clearConsoleLog: () => clearConsoleLog
9881
- });
9882
- import { randomUUID as randomUUID3 } from "crypto";
9883
- function logConsoleMessage(data) {
9884
- const db = getDatabase();
9885
- const id = randomUUID3();
9886
- db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
9887
- return getConsoleMessage(id);
9888
- }
9889
- function getConsoleMessage(id) {
9890
- const db = getDatabase();
9891
- return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
9892
- }
9893
- function getConsoleLog(sessionId, level) {
9894
- const db = getDatabase();
9895
- if (level) {
9896
- return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
9897
- }
9898
- return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
9899
- }
9900
- function clearConsoleLog(sessionId) {
9901
- const db = getDatabase();
9902
- db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
9903
- }
9904
- var init_console_log = __esm(() => {
9905
- init_schema();
9906
- });
9907
-
9908
9874
  // src/lib/dialogs.ts
9909
9875
  var exports_dialogs = {};
9910
9876
  __export(exports_dialogs, {
@@ -17526,7 +17492,7 @@ var init_snapshots = __esm(() => {
17526
17492
  });
17527
17493
 
17528
17494
  // src/server/index.ts
17529
- import { join as join12 } from "path";
17495
+ import { join as join13 } from "path";
17530
17496
  import { existsSync as existsSync9 } from "fs";
17531
17497
 
17532
17498
  // src/lib/session.ts
@@ -18576,6 +18542,16 @@ function selectEngine(useCase, explicit) {
18576
18542
  return preferred;
18577
18543
  }
18578
18544
 
18545
+ // src/lib/tui-recording.ts
18546
+ var recordingIntervals = new Map;
18547
+ function stopTuiRecording(sessionId) {
18548
+ const intervalId = recordingIntervals.get(sessionId);
18549
+ if (!intervalId)
18550
+ return;
18551
+ clearInterval(intervalId);
18552
+ recordingIntervals.delete(sessionId);
18553
+ }
18554
+
18579
18555
  // src/db/network-log.ts
18580
18556
  init_schema();
18581
18557
  import { randomUUID as randomUUID2 } from "crypto";
@@ -18704,8 +18680,28 @@ function startHAR(page) {
18704
18680
  };
18705
18681
  }
18706
18682
 
18683
+ // src/db/console-log.ts
18684
+ init_schema();
18685
+ import { randomUUID as randomUUID3 } from "crypto";
18686
+ function logConsoleMessage(data) {
18687
+ const db = getDatabase();
18688
+ const id = randomUUID3();
18689
+ db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
18690
+ return getConsoleMessage(id);
18691
+ }
18692
+ function getConsoleMessage(id) {
18693
+ const db = getDatabase();
18694
+ return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
18695
+ }
18696
+ function getConsoleLog(sessionId, level) {
18697
+ const db = getDatabase();
18698
+ if (level) {
18699
+ return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
18700
+ }
18701
+ return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
18702
+ }
18703
+
18707
18704
  // src/lib/console.ts
18708
- init_console_log();
18709
18705
  function enableConsoleCapture(page, sessionId) {
18710
18706
  const onConsole = (msg) => {
18711
18707
  const levelMap = {
@@ -18782,6 +18778,12 @@ if (!window.chrome.runtime) {
18782
18778
  id: undefined,
18783
18779
  };
18784
18780
  }
18781
+
18782
+ // \u2500\u2500 5. Override navigator.userAgent to match HTTP header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
18783
+ Object.defineProperty(navigator, 'userAgent', {
18784
+ get: () => "${REALISTIC_USER_AGENT}",
18785
+ configurable: true,
18786
+ });
18785
18787
  `;
18786
18788
  async function applyStealthPatches(page) {
18787
18789
  await page.context().addInitScript(STEALTH_SCRIPT);
@@ -18826,6 +18828,21 @@ if (dbPruneInterval.unref)
18826
18828
  function createBunProxy(view) {
18827
18829
  return view;
18828
18830
  }
18831
+ function attachPlaywrightListeners(page, sessionId, cleanups, opts = {}) {
18832
+ if (opts.captureNetwork !== false) {
18833
+ try {
18834
+ cleanups.push(enableNetworkLogging(page, sessionId));
18835
+ } catch {}
18836
+ }
18837
+ if (opts.captureConsole !== false) {
18838
+ try {
18839
+ cleanups.push(enableConsoleCapture(page, sessionId));
18840
+ } catch {}
18841
+ }
18842
+ try {
18843
+ cleanups.push(setupDialogHandler(page, sessionId));
18844
+ } catch {}
18845
+ }
18829
18846
  async function createSession2(opts = {}) {
18830
18847
  if (opts.cdpUrl) {
18831
18848
  const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
@@ -18863,9 +18880,11 @@ async function createSession2(opts = {}) {
18863
18880
  let browser = null;
18864
18881
  let bunView = null;
18865
18882
  let page;
18883
+ let actualEngine = resolvedEngine;
18866
18884
  if (resolvedEngine === "bun") {
18867
18885
  if (!isBunWebViewAvailable()) {
18868
18886
  console.warn("[browser] Bun.WebView requested but not available \u2014 falling back to playwright. Run: bun upgrade --canary");
18887
+ actualEngine = "playwright";
18869
18888
  browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
18870
18889
  page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
18871
18890
  } else {
@@ -18885,9 +18904,11 @@ async function createSession2(opts = {}) {
18885
18904
  }
18886
18905
  if (!bunWorks) {
18887
18906
  console.warn("[browser] Bun.WebView exists but Chrome not available \u2014 falling back to playwright");
18907
+ actualEngine = "playwright";
18888
18908
  browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
18889
18909
  page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
18890
18910
  } else {
18911
+ actualEngine = "bun";
18891
18912
  bunView = testView;
18892
18913
  if (opts.stealth) {}
18893
18914
  page = createBunProxy(bunView);
@@ -18917,19 +18938,10 @@ async function createSession2(opts = {}) {
18917
18938
  });
18918
18939
  const cleanups2 = [];
18919
18940
  cleanups2.push(() => closeTui(tuiSess));
18920
- if (opts.captureNetwork !== false) {
18921
- try {
18922
- cleanups2.push(enableNetworkLogging(page, session2.id));
18923
- } catch {}
18924
- }
18925
- if (opts.captureConsole !== false) {
18926
- try {
18927
- cleanups2.push(enableConsoleCapture(page, session2.id));
18928
- } catch {}
18929
- }
18930
- try {
18931
- cleanups2.push(setupDialogHandler(page, session2.id));
18932
- } catch {}
18941
+ attachPlaywrightListeners(page, session2.id, cleanups2, {
18942
+ captureNetwork: opts.captureNetwork,
18943
+ captureConsole: opts.captureConsole
18944
+ });
18933
18945
  handles.set(session2.id, { browser, bunView: null, tuiSession: tuiSess, page, engine: "tui", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "bash" });
18934
18946
  return { session: session2, page };
18935
18947
  } else {
@@ -18959,7 +18971,7 @@ async function createSession2(opts = {}) {
18959
18971
  }
18960
18972
  })() : undefined);
18961
18973
  const session = createSession({
18962
- engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
18974
+ engine: actualEngine,
18963
18975
  projectId: opts.projectId,
18964
18976
  agentId: opts.agentId,
18965
18977
  startUrl: opts.startUrl,
@@ -18972,37 +18984,12 @@ async function createSession2(opts = {}) {
18972
18984
  }
18973
18985
  const cleanups = [];
18974
18986
  if (!bunView) {
18975
- if (opts.captureNetwork !== false) {
18976
- try {
18977
- cleanups.push(enableNetworkLogging(page, session.id));
18978
- } catch {}
18979
- }
18980
- if (opts.captureConsole !== false) {
18981
- try {
18982
- cleanups.push(enableConsoleCapture(page, session.id));
18983
- } catch {}
18984
- }
18985
- try {
18986
- cleanups.push(setupDialogHandler(page, session.id));
18987
- } catch {}
18988
- } else {
18989
- if (opts.captureConsole !== false) {
18990
- try {
18991
- const { logConsoleMessage: logConsoleMessage2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
18992
- await bunView.addInitScript(`
18993
- (() => {
18994
- const orig = { log: console.log, warn: console.warn, error: console.error, debug: console.debug, info: console.info };
18995
- ['log','warn','error','debug','info'].forEach(level => {
18996
- console[level] = (...args) => {
18997
- orig[level](...args);
18998
- };
18999
- });
19000
- })()
19001
- `);
19002
- } catch {}
19003
- }
18987
+ attachPlaywrightListeners(page, session.id, cleanups, {
18988
+ captureNetwork: opts.captureNetwork,
18989
+ captureConsole: opts.captureConsole
18990
+ });
19004
18991
  }
19005
- handles.set(session.id, { browser, bunView, tuiSession: null, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "" });
18992
+ handles.set(session.id, { browser, bunView, tuiSession: null, page, engine: actualEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "" });
19006
18993
  if (opts.startUrl) {
19007
18994
  try {
19008
18995
  if (bunView) {
@@ -19035,9 +19022,12 @@ async function closeSession2(sessionId) {
19035
19022
  const handle = handles.get(sessionId);
19036
19023
  try {
19037
19024
  if (handle) {
19025
+ if (handle.engine === "tui") {
19026
+ stopTuiRecording(sessionId);
19027
+ }
19038
19028
  for (const cleanup of handle.cleanups) {
19039
19029
  try {
19040
- cleanup();
19030
+ await cleanup();
19041
19031
  } catch {}
19042
19032
  }
19043
19033
  if (handle.bunView) {
@@ -19162,16 +19152,23 @@ async function extract(page, opts = {}) {
19162
19152
  // src/lib/screenshot.ts
19163
19153
  init_types();
19164
19154
  var import_sharp = __toESM(require_lib3(), 1);
19165
- import { join as join9 } from "path";
19155
+ import { join as join10 } from "path";
19166
19156
  import { mkdirSync as mkdirSync7 } from "fs";
19167
19157
 
19168
19158
  // src/db/gallery.ts
19169
19159
  init_schema();
19170
19160
  import { randomUUID as randomUUID4 } from "crypto";
19161
+ import { join as join9, resolve, relative as relative2, isAbsolute } from "path";
19171
19162
  function validateDataPath(filePath) {
19172
19163
  if (filePath.includes("..")) {
19173
19164
  throw new Error(`File path must not contain '..': ${filePath}`);
19174
19165
  }
19166
+ const dataDir = resolve(getDataDir2());
19167
+ const resolved = resolve(isAbsolute(filePath) ? filePath : join9(dataDir, filePath));
19168
+ const rel = relative2(dataDir, resolved);
19169
+ if (rel.startsWith("..") || isAbsolute(rel)) {
19170
+ throw new Error(`File path must be within data directory: ${filePath}`);
19171
+ }
19175
19172
  return filePath;
19176
19173
  }
19177
19174
  function deserialize(row) {
@@ -19189,7 +19186,13 @@ function deserialize(row) {
19189
19186
  original_size_bytes: row.original_size_bytes ?? undefined,
19190
19187
  compressed_size_bytes: row.compressed_size_bytes ?? undefined,
19191
19188
  compression_ratio: row.compression_ratio ?? undefined,
19192
- tags: JSON.parse(row.tags),
19189
+ tags: (() => {
19190
+ try {
19191
+ return JSON.parse(row.tags);
19192
+ } catch {
19193
+ return [];
19194
+ }
19195
+ })(),
19193
19196
  notes: row.notes ?? undefined,
19194
19197
  is_favorite: row.is_favorite === 1,
19195
19198
  created_at: row.created_at
@@ -19303,9 +19306,9 @@ function getGalleryStats(projectId) {
19303
19306
  // src/lib/screenshot.ts
19304
19307
  init_schema();
19305
19308
  function getScreenshotDir(projectId) {
19306
- const base = join9(getDataDir2(), "screenshots");
19309
+ const base = join10(getDataDir2(), "screenshots");
19307
19310
  const date = new Date().toISOString().split("T")[0];
19308
- const dir = projectId ? join9(base, projectId, date) : join9(base, date);
19311
+ const dir = projectId ? join10(base, projectId, date) : join10(base, date);
19309
19312
  mkdirSync7(dir, { recursive: true });
19310
19313
  return dir;
19311
19314
  }
@@ -19321,7 +19324,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
19321
19324
  }
19322
19325
  }
19323
19326
  async function generateThumbnail(raw, dir, stem) {
19324
- const thumbPath = join9(dir, `${stem}.thumb.webp`);
19327
+ const thumbPath = join10(dir, `${stem}.thumb.webp`);
19325
19328
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
19326
19329
  await Bun.write(thumbPath, thumbBuffer);
19327
19330
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -19389,7 +19392,7 @@ async function takeScreenshot(page, opts) {
19389
19392
  const compressedSizeBytes = finalBuffer.length;
19390
19393
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
19391
19394
  const ext = format;
19392
- const screenshotPath = opts?.path ?? join9(dir, `${stem}.${ext}`);
19395
+ const screenshotPath = opts?.path ?? join10(dir, `${stem}.${ext}`);
19393
19396
  await Bun.write(screenshotPath, finalBuffer);
19394
19397
  let thumbnailPath;
19395
19398
  let thumbnailBase64;
@@ -19679,16 +19682,15 @@ function listProjects() {
19679
19682
  }
19680
19683
 
19681
19684
  // src/server/index.ts
19682
- init_console_log();
19683
19685
  init_recordings();
19684
19686
 
19685
19687
  // src/lib/downloads.ts
19686
19688
  init_schema();
19687
- import { join as join10, basename, extname } from "path";
19689
+ import { join as join11, basename, extname } from "path";
19688
19690
  import { mkdirSync as mkdirSync8, existsSync as existsSync7, readdirSync as readdirSync6, statSync as statSync2, unlinkSync as unlinkSync2, copyFileSync as copyFileSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
19689
19691
  function getDownloadsDir(sessionId) {
19690
- const base = join10(getDataDir2(), "downloads");
19691
- const dir = sessionId ? join10(base, sessionId) : base;
19692
+ const base = join11(getDataDir2(), "downloads");
19693
+ const dir = sessionId ? join11(base, sessionId) : base;
19692
19694
  mkdirSync8(dir, { recursive: true });
19693
19695
  return dir;
19694
19696
  }
@@ -19705,7 +19707,7 @@ function listDownloads(sessionId) {
19705
19707
  for (const entry of entries) {
19706
19708
  if (entry.endsWith(".meta.json"))
19707
19709
  continue;
19708
- const full = join10(d, entry);
19710
+ const full = join11(d, entry);
19709
19711
  const stat = statSync2(full);
19710
19712
  if (stat.isDirectory()) {
19711
19713
  scanDir(full);
@@ -19767,9 +19769,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
19767
19769
  // src/lib/gallery-diff.ts
19768
19770
  init_schema();
19769
19771
  var import_sharp2 = __toESM(require_lib3(), 1);
19770
- import { join as join11 } from "path";
19772
+ import { join as join12 } from "path";
19771
19773
  import { mkdirSync as mkdirSync9 } from "fs";
19772
- async function diffImages(path1, path2) {
19774
+ async function diffImages(path1, path2, threshold = 10) {
19773
19775
  const img1 = import_sharp2.default(path1);
19774
19776
  const img2 = import_sharp2.default(path2);
19775
19777
  const [meta1, meta2] = await Promise.all([img1.metadata(), img2.metadata()]);
@@ -19788,7 +19790,7 @@ async function diffImages(path1, path2) {
19788
19790
  const dg = Math.abs(raw1[i + 1] - raw2[i + 1]);
19789
19791
  const db = Math.abs(raw1[i + 2] - raw2[i + 2]);
19790
19792
  const diff = (dr + dg + db) / 3;
19791
- if (diff > 10) {
19793
+ if (diff > threshold) {
19792
19794
  changedPixels++;
19793
19795
  diffBuffer[i] = 255;
19794
19796
  diffBuffer[i + 1] = 0;
@@ -19800,9 +19802,9 @@ async function diffImages(path1, path2) {
19800
19802
  }
19801
19803
  }
19802
19804
  const dataDir = getDataDir2();
19803
- const diffDir = join11(dataDir, "diffs");
19805
+ const diffDir = join12(dataDir, "diffs");
19804
19806
  mkdirSync9(diffDir, { recursive: true });
19805
- const diffPath = join11(diffDir, `diff-${Date.now()}.webp`);
19807
+ const diffPath = join12(diffDir, `diff-${Date.now()}.webp`);
19806
19808
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
19807
19809
  await Bun.write(diffPath, diffImageBuffer);
19808
19810
  return {
@@ -20182,19 +20184,19 @@ var server = Bun.serve({
20182
20184
  const id = path.split("/")[3];
20183
20185
  return ok({ deleted: deleteDownload(id) });
20184
20186
  }
20185
- const dashboardDist = join12(import.meta.dir, "../../dashboard/dist");
20187
+ const dashboardDist = join13(import.meta.dir, "../../dashboard/dist");
20186
20188
  if (existsSync9(dashboardDist)) {
20187
20189
  const cleanPath = path.replace(/^\//, "");
20188
20190
  if (cleanPath.includes("..") || cleanPath.startsWith("/"))
20189
20191
  return notFound("Not found", headers);
20190
- const filePath = path === "/" ? join12(dashboardDist, "index.html") : join12(dashboardDist, cleanPath);
20191
- const resolved = await Bun.file(filePath).arrayBuffer().then(() => join12(dashboardDist, cleanPath)) || "";
20192
+ const filePath = path === "/" ? join13(dashboardDist, "index.html") : join13(dashboardDist, cleanPath);
20193
+ const resolved = await Bun.file(filePath).arrayBuffer().then(() => join13(dashboardDist, cleanPath)) || "";
20192
20194
  if (!resolved.startsWith(dashboardDist))
20193
20195
  return notFound("Not found", headers);
20194
20196
  if (existsSync9(filePath)) {
20195
20197
  return new Response(Bun.file(filePath), { headers });
20196
20198
  }
20197
- return new Response(Bun.file(join12(dashboardDist, "index.html")), { headers });
20199
+ return new Response(Bun.file(join13(dashboardDist, "index.html")), { headers });
20198
20200
  }
20199
20201
  if (path === "/" || path === "") {
20200
20202
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/browser",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "General-purpose browser agent toolkit — Playwright, Chrome DevTools Protocol, Lightpanda with auto engine selection. CLI + MCP + REST + SDK.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",