mcp-scraper 0.1.9 → 0.2.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.
@@ -3530,8 +3530,8 @@ var init_url_utils = __esm({
3530
3530
  async function fetchWithKernel(url) {
3531
3531
  const apiKey = browserServiceApiKey();
3532
3532
  if (!apiKey) throw new Error("Browser backend API key not set");
3533
- const client = new import_sdk.default({ apiKey });
3534
- const kb = await client.browsers.create({ stealth: true, timeout_seconds: 60 });
3533
+ const client2 = new import_sdk.default({ apiKey });
3534
+ const kb = await client2.browsers.create({ stealth: true, timeout_seconds: 60 });
3535
3535
  const browser = await import_playwright.chromium.connectOverCDP(kb.cdp_ws_url);
3536
3536
  try {
3537
3537
  const context = browser.contexts()[0] ?? await browser.newContext({
@@ -3544,7 +3544,7 @@ async function fetchWithKernel(url) {
3544
3544
  } finally {
3545
3545
  await browser.close().catch(() => {
3546
3546
  });
3547
- await client.browsers.deleteByID(kb.session_id).catch(() => {
3547
+ await client2.browsers.deleteByID(kb.session_id).catch(() => {
3548
3548
  });
3549
3549
  }
3550
3550
  }
@@ -8292,6 +8292,9 @@ var init_site_audit_routes = __esm({
8292
8292
  });
8293
8293
 
8294
8294
  // src/api/rates.ts
8295
+ function browserActiveCostMc(activeMs) {
8296
+ return Math.round(activeMs * MC_PER_BROWSER_MS);
8297
+ }
8295
8298
  function mcToCredits(mc) {
8296
8299
  return mc / MC_PER_CREDIT;
8297
8300
  }
@@ -8308,7 +8311,7 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
8308
8311
  topup_url: topupUrl
8309
8312
  };
8310
8313
  }
8311
- var MC_COSTS, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, FREE_SIGNUP_MC, FREE_MONTHLY_REFRESH_MC, BALANCE_PRICE_IDS, BALANCE_PACK_LABELS, LedgerOperation;
8314
+ var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, FREE_SIGNUP_MC, FREE_MONTHLY_REFRESH_MC, BALANCE_PRICE_IDS, BALANCE_PACK_LABELS, LedgerOperation;
8312
8315
  var init_rates = __esm({
8313
8316
  "src/api/rates.ts"() {
8314
8317
  "use strict";
@@ -8324,8 +8327,11 @@ var init_rates = __esm({
8324
8327
  maps_place: 2e3,
8325
8328
  maps_review: 50,
8326
8329
  fb_search: 50,
8327
- fb_transcribe: 50
8330
+ fb_transcribe: 50,
8331
+ browser_minute: 4e3
8328
8332
  };
8333
+ MC_PER_BROWSER_MS = MC_COSTS.browser_minute / 6e4;
8334
+ BROWSER_OPEN_MIN_BALANCE_MC = 1e3;
8329
8335
  MC_PER_CREDIT = 1e3;
8330
8336
  CREDIT_COST_CATALOG = [
8331
8337
  {
@@ -8421,6 +8427,14 @@ var init_rates = __esm({
8421
8427
  credits: mcToCredits(MC_COSTS.fb_transcribe),
8422
8428
  unit: "per call",
8423
8429
  notes: "Whisper transcription of Facebook ad video via fal.ai."
8430
+ },
8431
+ {
8432
+ key: "browser_minute",
8433
+ label: "Interactive browser session",
8434
+ aliases: ["browser_open", "browser agent", "browser_agent", "live browser", "browse", "browser control", "interactive browser"],
8435
+ credits: mcToCredits(MC_COSTS.browser_minute),
8436
+ unit: "per minute of active time",
8437
+ notes: "Metered per second of active browser work (navigation, clicks, typing, screenshots). Idle and standby time are free. Billed against your balance as you act; close the session to stop the meter."
8424
8438
  }
8425
8439
  ];
8426
8440
  CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
@@ -8466,7 +8480,8 @@ var init_rates = __esm({
8466
8480
  FB_SEARCH: "fb_search",
8467
8481
  FB_TRANSCRIBE: "fb_transcribe",
8468
8482
  FB_SEARCH_REFUND: "fb_search_refund",
8469
- FB_TRANSCRIBE_REFUND: "fb_transcribe_refund"
8483
+ FB_TRANSCRIBE_REFUND: "fb_transcribe_refund",
8484
+ BROWSER_SESSION: "browser_session"
8470
8485
  };
8471
8486
  }
8472
8487
  });
@@ -9032,19 +9047,19 @@ var init_BrowserDriver = __esm({
9032
9047
  if (this.browser) {
9033
9048
  const b = this.browser;
9034
9049
  const sessionId = this.kernelSessionId;
9035
- const client = this.kernelClient;
9050
+ const client2 = this.kernelClient;
9036
9051
  this.browser = null;
9037
9052
  this.context = null;
9038
9053
  this.page = null;
9039
9054
  this.kernelSessionId = null;
9040
9055
  this.kernelClient = null;
9041
- if (client && sessionId) {
9056
+ if (client2 && sessionId) {
9042
9057
  console.info(JSON.stringify({
9043
9058
  event: "kernel_browser_delete_started",
9044
9059
  kernel_session_id: sessionId
9045
9060
  }));
9046
9061
  const deleteSession = withTimeout(
9047
- client.browsers.deleteByID(sessionId),
9062
+ client2.browsers.deleteByID(sessionId),
9048
9063
  KERNEL_SESSION_DELETE_TIMEOUT_MS,
9049
9064
  `Kernel session ${sessionId} delete`
9050
9065
  );
@@ -10513,13 +10528,13 @@ var init_FacebookAdExtractor = __esm({
10513
10528
  }
10514
10529
  await page.waitForTimeout(1500);
10515
10530
  let prevCount = 0;
10516
- for (let scroll = 0; scroll < 20; scroll++) {
10531
+ for (let scroll2 = 0; scroll2 < 20; scroll2++) {
10517
10532
  const count = await page.evaluate(() => {
10518
10533
  const bt = document.body ? document.body.innerText ?? "" : "";
10519
10534
  return [...bt.matchAll(/Library ID/g)].length;
10520
10535
  });
10521
10536
  if (count >= maxAds) break;
10522
- if (count === prevCount && scroll > 0) break;
10537
+ if (count === prevCount && scroll2 > 0) break;
10523
10538
  prevCount = count;
10524
10539
  await page.evaluate(() => {
10525
10540
  if (document.body) window.scrollTo(0, document.body.scrollHeight);
@@ -11663,7 +11678,7 @@ var init_facebook_ad_routes = __esm({
11663
11678
  return c.json(searchResult2);
11664
11679
  }
11665
11680
  await page.waitForTimeout(1500);
11666
- for (let scroll = 0; scroll < 3; scroll++) {
11681
+ for (let scroll2 = 0; scroll2 < 3; scroll2++) {
11667
11682
  await page.evaluate(() => {
11668
11683
  if (document.body) window.scrollTo(0, document.body.scrollHeight);
11669
11684
  });
@@ -15276,7 +15291,7 @@ var PACKAGE_VERSION;
15276
15291
  var init_version = __esm({
15277
15292
  "src/version.ts"() {
15278
15293
  "use strict";
15279
- PACKAGE_VERSION = "0.1.9";
15294
+ PACKAGE_VERSION = "0.2.0";
15280
15295
  }
15281
15296
  });
15282
15297
 
@@ -16809,17 +16824,802 @@ var init_mcp_routes = __esm({
16809
16824
  }
16810
16825
  });
16811
16826
 
16827
+ // src/api/browser-agent-db.ts
16828
+ async function migrateBrowserAgent() {
16829
+ if (_ready) return;
16830
+ const db = getDb();
16831
+ await db.execute(`
16832
+ CREATE TABLE IF NOT EXISTS browser_agent_sessions (
16833
+ id TEXT PRIMARY KEY,
16834
+ runtime_session_id TEXT NOT NULL,
16835
+ live_view_url TEXT,
16836
+ cdp_ws_url TEXT NOT NULL,
16837
+ status TEXT NOT NULL DEFAULT 'open',
16838
+ label TEXT,
16839
+ user_id INTEGER,
16840
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
16841
+ closed_at TEXT,
16842
+ last_action_at TEXT,
16843
+ active_ms INTEGER NOT NULL DEFAULT 0,
16844
+ billed_mc INTEGER NOT NULL DEFAULT 0
16845
+ )
16846
+ `);
16847
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_sessions_status ON browser_agent_sessions(status)`);
16848
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_sessions_user ON browser_agent_sessions(user_id)`);
16849
+ await db.execute(`
16850
+ CREATE TABLE IF NOT EXISTS browser_agent_actions (
16851
+ id TEXT PRIMARY KEY,
16852
+ session_id TEXT NOT NULL,
16853
+ type TEXT NOT NULL,
16854
+ params_json TEXT,
16855
+ ok INTEGER NOT NULL DEFAULT 1,
16856
+ error TEXT,
16857
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
16858
+ )
16859
+ `);
16860
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_actions_session ON browser_agent_actions(session_id)`);
16861
+ await db.execute(`
16862
+ CREATE TABLE IF NOT EXISTS browser_agent_replays (
16863
+ replay_id TEXT PRIMARY KEY,
16864
+ session_id TEXT NOT NULL,
16865
+ view_url TEXT,
16866
+ label TEXT,
16867
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
16868
+ stopped_at TEXT
16869
+ )
16870
+ `);
16871
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_replays_session ON browser_agent_replays(session_id)`);
16872
+ _ready = true;
16873
+ }
16874
+ async function createSessionRow(input) {
16875
+ const db = getDb();
16876
+ const id = `bas_${(0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
16877
+ await db.execute({
16878
+ sql: `INSERT INTO browser_agent_sessions (id, runtime_session_id, live_view_url, cdp_ws_url, status, label, user_id, last_action_at)
16879
+ VALUES (?, ?, ?, ?, 'open', ?, ?, datetime('now'))`,
16880
+ args: [id, input.runtimeSessionId, input.liveViewUrl, input.cdpWsUrl, input.label, input.userId]
16881
+ });
16882
+ const row = await getSessionRow(id);
16883
+ if (!row) throw new Error("session insert failed");
16884
+ return row;
16885
+ }
16886
+ async function getSessionRow(id) {
16887
+ const db = getDb();
16888
+ const res = await db.execute({ sql: `SELECT * FROM browser_agent_sessions WHERE id = ?`, args: [id] });
16889
+ return res.rows[0] ?? null;
16890
+ }
16891
+ async function listSessionRows(userId, includeClosed = false) {
16892
+ const db = getDb();
16893
+ const clauses = [];
16894
+ const args = [];
16895
+ if (userId != null) {
16896
+ clauses.push("(user_id = ? OR user_id IS NULL)");
16897
+ args.push(userId);
16898
+ }
16899
+ if (!includeClosed) clauses.push(`status = 'open'`);
16900
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
16901
+ const res = await db.execute({
16902
+ sql: `SELECT * FROM browser_agent_sessions ${where} ORDER BY created_at DESC LIMIT 100`,
16903
+ args
16904
+ });
16905
+ return res.rows;
16906
+ }
16907
+ async function addActiveMs(id, deltaMs) {
16908
+ const db = getDb();
16909
+ await db.execute({
16910
+ sql: `UPDATE browser_agent_sessions SET active_ms = active_ms + ?, last_action_at = datetime('now') WHERE id = ?`,
16911
+ args: [Math.max(0, Math.round(deltaMs)), id]
16912
+ });
16913
+ const res = await db.execute({ sql: `SELECT active_ms, billed_mc FROM browser_agent_sessions WHERE id = ?`, args: [id] });
16914
+ const row = res.rows[0];
16915
+ return { active_ms: Number(row?.active_ms ?? 0), billed_mc: Number(row?.billed_mc ?? 0) };
16916
+ }
16917
+ async function setBilledMc(id, billedMc) {
16918
+ const db = getDb();
16919
+ await db.execute({
16920
+ sql: `UPDATE browser_agent_sessions SET billed_mc = ? WHERE id = ?`,
16921
+ args: [Math.round(billedMc), id]
16922
+ });
16923
+ }
16924
+ async function markSessionClosed(id) {
16925
+ const db = getDb();
16926
+ await db.execute({
16927
+ sql: `UPDATE browser_agent_sessions SET status = 'closed', closed_at = datetime('now') WHERE id = ?`,
16928
+ args: [id]
16929
+ });
16930
+ }
16931
+ async function recordAction(input) {
16932
+ const db = getDb();
16933
+ await db.execute({
16934
+ sql: `INSERT INTO browser_agent_actions (id, session_id, type, params_json, ok, error)
16935
+ VALUES (?, ?, ?, ?, ?, ?)`,
16936
+ args: [
16937
+ `baa_${(0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 20)}`,
16938
+ input.sessionId,
16939
+ input.type,
16940
+ input.params == null ? null : JSON.stringify(input.params),
16941
+ input.ok ? 1 : 0,
16942
+ input.error ?? null
16943
+ ]
16944
+ });
16945
+ }
16946
+ async function recordReplayStart(input) {
16947
+ const db = getDb();
16948
+ await db.execute({
16949
+ sql: `INSERT INTO browser_agent_replays (replay_id, session_id, view_url, label)
16950
+ VALUES (?, ?, ?, ?)`,
16951
+ args: [input.replayId, input.sessionId, input.viewUrl, input.label]
16952
+ });
16953
+ }
16954
+ async function recordReplayStop(replayId, viewUrl) {
16955
+ const db = getDb();
16956
+ await db.execute({
16957
+ sql: `UPDATE browser_agent_replays SET stopped_at = datetime('now'), view_url = COALESCE(?, view_url) WHERE replay_id = ?`,
16958
+ args: [viewUrl, replayId]
16959
+ });
16960
+ }
16961
+ async function listReplayRows(sessionId) {
16962
+ const db = getDb();
16963
+ const res = await db.execute({
16964
+ sql: `SELECT * FROM browser_agent_replays WHERE session_id = ? ORDER BY started_at DESC`,
16965
+ args: [sessionId]
16966
+ });
16967
+ return res.rows;
16968
+ }
16969
+ var import_node_crypto3, _ready;
16970
+ var init_browser_agent_db = __esm({
16971
+ "src/api/browser-agent-db.ts"() {
16972
+ "use strict";
16973
+ import_node_crypto3 = require("crypto");
16974
+ init_db();
16975
+ _ready = false;
16976
+ }
16977
+ });
16978
+
16979
+ // src/services/browser-agent/browser-agent-service.ts
16980
+ function client() {
16981
+ const apiKey = browserServiceApiKey();
16982
+ if (!apiKey) throw new Error("Browser backend API key is required");
16983
+ return new import_sdk6.default({ apiKey });
16984
+ }
16985
+ async function createSession(opts = {}) {
16986
+ const k = client();
16987
+ const resolvedProxyId = opts.proxyId ?? browserServiceProxyId();
16988
+ const browser = await k.browsers.create({
16989
+ stealth: opts.stealth ?? true,
16990
+ timeout_seconds: opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS,
16991
+ ...resolvedProxyId ? { proxy_id: resolvedProxyId } : {},
16992
+ ...opts.profileName ? { profile: { name: opts.profileName } } : {}
16993
+ });
16994
+ const runtimeSessionId = browser.session_id;
16995
+ if (opts.disableDefaultProxy) {
16996
+ try {
16997
+ await k.browsers.update(runtimeSessionId, { disable_default_proxy: true });
16998
+ } catch {
16999
+ }
17000
+ }
17001
+ if (opts.viewport) {
17002
+ try {
17003
+ await k.browsers.update(runtimeSessionId, { viewport: opts.viewport });
17004
+ } catch {
17005
+ }
17006
+ }
17007
+ return {
17008
+ runtimeSessionId,
17009
+ liveViewUrl: browser.browser_live_view_url ?? null,
17010
+ cdpWsUrl: browser.cdp_ws_url
17011
+ };
17012
+ }
17013
+ async function closeSession(runtimeSessionId) {
17014
+ const k = client();
17015
+ await k.browsers.deleteByID(runtimeSessionId);
17016
+ }
17017
+ async function screenshot(runtimeSessionId) {
17018
+ const k = client();
17019
+ const res = await k.browsers.computer.captureScreenshot(runtimeSessionId);
17020
+ const buf = Buffer.from(await res.arrayBuffer());
17021
+ return { base64: buf.toString("base64"), mimeType: "image/png" };
17022
+ }
17023
+ async function click(runtimeSessionId, x, y, opts = {}) {
17024
+ const k = client();
17025
+ await k.browsers.computer.clickMouse(runtimeSessionId, {
17026
+ x,
17027
+ y,
17028
+ button: opts.button ?? "left",
17029
+ ...opts.numClicks ? { num_clicks: opts.numClicks } : {}
17030
+ });
17031
+ }
17032
+ async function typeText(runtimeSessionId, text, delayMs) {
17033
+ const k = client();
17034
+ await k.browsers.computer.typeText(runtimeSessionId, {
17035
+ text,
17036
+ ...typeof delayMs === "number" ? { delay: delayMs } : {}
17037
+ });
17038
+ }
17039
+ async function scroll(runtimeSessionId, x, y, deltaX, deltaY) {
17040
+ const k = client();
17041
+ await k.browsers.computer.scroll(runtimeSessionId, { x, y, delta_x: deltaX, delta_y: deltaY });
17042
+ }
17043
+ async function pressKeys(runtimeSessionId, keys) {
17044
+ const k = client();
17045
+ await k.browsers.computer.pressKey(runtimeSessionId, { keys });
17046
+ }
17047
+ async function goto(cdpWsUrl, url) {
17048
+ const browser = await import_playwright4.chromium.connectOverCDP(cdpWsUrl);
17049
+ try {
17050
+ const context = browser.contexts()[0] ?? await browser.newContext();
17051
+ const page = context.pages()[0] ?? await context.newPage();
17052
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
17053
+ return { url: page.url(), title: await page.title() };
17054
+ } finally {
17055
+ await browser.close().catch(() => {
17056
+ });
17057
+ }
17058
+ }
17059
+ async function readPage(cdpWsUrl) {
17060
+ const browser = await import_playwright4.chromium.connectOverCDP(cdpWsUrl);
17061
+ try {
17062
+ const context = browser.contexts()[0] ?? await browser.newContext();
17063
+ const page = context.pages()[0] ?? await context.newPage();
17064
+ const url = page.url();
17065
+ const title = await page.title().catch(() => "");
17066
+ const data = await page.evaluate(() => {
17067
+ const SEL = 'a[href], button, input, textarea, select, [role="button"], [role="link"], [role="tab"], [onclick]';
17068
+ const out = [];
17069
+ const nodes = Array.from(document.querySelectorAll(SEL)).slice(0, 120);
17070
+ for (const el2 of nodes) {
17071
+ const r = el2.getBoundingClientRect();
17072
+ if (r.width < 1 || r.height < 1) continue;
17073
+ if (r.bottom < 0 || r.top > window.innerHeight) continue;
17074
+ const e = el2;
17075
+ const name = (e.getAttribute("aria-label") || e.placeholder || e.innerText || e.getAttribute("value") || e.getAttribute("title") || "").trim().replace(/\s+/g, " ").slice(0, 80);
17076
+ out.push({
17077
+ role: el2.getAttribute("role") || el2.tagName.toLowerCase(),
17078
+ name,
17079
+ x: Math.round(r.left + r.width / 2),
17080
+ y: Math.round(r.top + r.height / 2)
17081
+ });
17082
+ }
17083
+ const text = (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").trim().slice(0, 6e3);
17084
+ return { text, els: out };
17085
+ });
17086
+ return {
17087
+ url,
17088
+ title,
17089
+ text: data.text,
17090
+ elements: data.els.map((e, i) => ({ ref: i + 1, role: e.role, name: e.name, x: e.x, y: e.y }))
17091
+ };
17092
+ } finally {
17093
+ await browser.close().catch(() => {
17094
+ });
17095
+ }
17096
+ }
17097
+ async function replayStart(runtimeSessionId) {
17098
+ const k = client();
17099
+ const res = await k.browsers.replays.start(runtimeSessionId);
17100
+ return { replayId: res.replay_id, viewUrl: res.replay_view_url ?? null };
17101
+ }
17102
+ async function replayStop(runtimeSessionId, replayId) {
17103
+ const k = client();
17104
+ await k.browsers.replays.stop(replayId, { id: runtimeSessionId });
17105
+ }
17106
+ async function replayList(runtimeSessionId) {
17107
+ const k = client();
17108
+ const res = await k.browsers.replays.list(runtimeSessionId);
17109
+ return res.map((r) => ({
17110
+ replayId: r.replay_id,
17111
+ viewUrl: r.replay_view_url ?? null,
17112
+ startedAt: r.started_at ?? null,
17113
+ finishedAt: r.finished_at ?? null
17114
+ }));
17115
+ }
17116
+ var import_sdk6, import_playwright4, DEFAULT_TIMEOUT_SECONDS;
17117
+ var init_browser_agent_service = __esm({
17118
+ "src/services/browser-agent/browser-agent-service.ts"() {
17119
+ "use strict";
17120
+ import_sdk6 = __toESM(require("@onkernel/sdk"), 1);
17121
+ import_playwright4 = require("playwright");
17122
+ init_browser_service_env();
17123
+ DEFAULT_TIMEOUT_SECONDS = 600;
17124
+ }
17125
+ });
17126
+
17127
+ // src/api/browser-agent-routes.ts
17128
+ async function charge(sessionId, userId, startedAtMs) {
17129
+ const elapsedMs = Date.now() - startedAtMs;
17130
+ const { active_ms, billed_mc } = await addActiveMs(sessionId, elapsedMs);
17131
+ const owed = browserActiveCostMc(active_ms);
17132
+ const delta = owed - billed_mc;
17133
+ if (delta > 0) {
17134
+ const res = await debitMc(userId, delta, LedgerOperation.BROWSER_SESSION, sessionId);
17135
+ if (res.ok) await setBilledMc(sessionId, owed);
17136
+ }
17137
+ }
17138
+ function publicSession(row) {
17139
+ return {
17140
+ session_id: row.id,
17141
+ status: row.status,
17142
+ label: row.label,
17143
+ created_at: row.created_at,
17144
+ last_action_at: row.last_action_at,
17145
+ closed_at: row.closed_at,
17146
+ active_seconds: Math.round((row.active_ms ?? 0) / 1e3),
17147
+ credits_used: Math.round((row.billed_mc ?? 0) / 10) / 100
17148
+ };
17149
+ }
17150
+ function failure(err) {
17151
+ const msg = err instanceof Error ? err.message : String(err);
17152
+ return { error: sanitizeVendorName(msg) };
17153
+ }
17154
+ async function loadOpenSession(id, userId) {
17155
+ const row = await getSessionRow(id);
17156
+ if (!row) return null;
17157
+ if (row.user_id != null && row.user_id !== userId) return null;
17158
+ return row;
17159
+ }
17160
+ function buildBrowserAgentRoutes() {
17161
+ const app2 = new import_hono8.Hono();
17162
+ app2.use("*", async (c, next) => {
17163
+ await migrateBrowserAgent();
17164
+ return next();
17165
+ });
17166
+ app2.use("*", auth);
17167
+ app2.post("/sessions", async (c) => {
17168
+ const user = c.get("user");
17169
+ if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
17170
+ return c.json(insufficientBalanceResponse(Number(user.balance_mc ?? 0), BROWSER_OPEN_MIN_BALANCE_MC), 402);
17171
+ }
17172
+ const body = await c.req.json().catch(() => ({}));
17173
+ try {
17174
+ const created = await createSession({
17175
+ timeoutSeconds: typeof body.timeout_seconds === "number" ? body.timeout_seconds : void 0,
17176
+ proxyId: typeof body.proxy_id === "string" ? body.proxy_id : void 0,
17177
+ profileName: typeof body.profile === "string" ? body.profile : void 0,
17178
+ disableDefaultProxy: body.disable_default_proxy === true,
17179
+ viewport: body.viewport && typeof body.viewport === "object" ? body.viewport : void 0
17180
+ });
17181
+ const row = await createSessionRow({
17182
+ runtimeSessionId: created.runtimeSessionId,
17183
+ liveViewUrl: created.liveViewUrl,
17184
+ cdpWsUrl: created.cdpWsUrl,
17185
+ label: typeof body.label === "string" ? body.label : null,
17186
+ userId: user.id
17187
+ });
17188
+ return c.json({ ...publicSession(row), watch_url: `/console/${row.id}` });
17189
+ } catch (err) {
17190
+ return c.json(failure(err), 502);
17191
+ }
17192
+ });
17193
+ app2.get("/sessions", async (c) => {
17194
+ const user = c.get("user");
17195
+ const includeClosed = c.req.query("all") === "1";
17196
+ const rows = await listSessionRows(user.id, includeClosed);
17197
+ return c.json({ sessions: rows.map(publicSession) });
17198
+ });
17199
+ app2.get("/sessions/:id", async (c) => {
17200
+ const user = c.get("user");
17201
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17202
+ if (!row) return c.json({ error: "not found" }, 404);
17203
+ return c.json({ ...publicSession(row), watch_url: `/console/${row.id}` });
17204
+ });
17205
+ app2.get("/sessions/:id/live-view", async (c) => {
17206
+ const user = c.get("user");
17207
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17208
+ if (!row) return c.json({ error: "not found" }, 404);
17209
+ return c.json({ live_view_url: row.live_view_url });
17210
+ });
17211
+ app2.delete("/sessions/:id", async (c) => {
17212
+ const user = c.get("user");
17213
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17214
+ if (!row) return c.json({ error: "not found" }, 404);
17215
+ try {
17216
+ await closeSession(row.runtime_session_id);
17217
+ } catch {
17218
+ }
17219
+ await markSessionClosed(row.id);
17220
+ return c.json({ ok: true });
17221
+ });
17222
+ app2.post("/sessions/:id/goto", async (c) => {
17223
+ const user = c.get("user");
17224
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17225
+ if (!row) return c.json({ error: "not found" }, 404);
17226
+ const body = await c.req.json().catch(() => ({}));
17227
+ const url = typeof body.url === "string" ? body.url : "";
17228
+ if (!url) return c.json({ error: "url is required" }, 400);
17229
+ const t0 = Date.now();
17230
+ try {
17231
+ const result = await goto(row.cdp_ws_url, url);
17232
+ await charge(row.id, user.id, t0);
17233
+ await recordAction({ sessionId: row.id, type: "goto", params: { url }, ok: true });
17234
+ return c.json(result);
17235
+ } catch (err) {
17236
+ await charge(row.id, user.id, t0);
17237
+ await recordAction({ sessionId: row.id, type: "goto", params: { url }, ok: false, error: String(err) });
17238
+ return c.json(failure(err), 502);
17239
+ }
17240
+ });
17241
+ app2.post("/sessions/:id/screenshot", async (c) => {
17242
+ const user = c.get("user");
17243
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17244
+ if (!row) return c.json({ error: "not found" }, 404);
17245
+ const t0 = Date.now();
17246
+ try {
17247
+ const shot = await screenshot(row.runtime_session_id);
17248
+ let page = null;
17249
+ try {
17250
+ page = await readPage(row.cdp_ws_url);
17251
+ } catch {
17252
+ page = null;
17253
+ }
17254
+ await charge(row.id, user.id, t0);
17255
+ await recordAction({ sessionId: row.id, type: "screenshot", params: null, ok: true });
17256
+ return c.json({
17257
+ image_base64: shot.base64,
17258
+ mime_type: shot.mimeType,
17259
+ url: page?.url ?? null,
17260
+ title: page?.title ?? null,
17261
+ elements: page?.elements ?? [],
17262
+ text: page?.text ?? null
17263
+ });
17264
+ } catch (err) {
17265
+ await charge(row.id, user.id, t0);
17266
+ await recordAction({ sessionId: row.id, type: "screenshot", params: null, ok: false, error: String(err) });
17267
+ return c.json(failure(err), 502);
17268
+ }
17269
+ });
17270
+ app2.post("/sessions/:id/read", async (c) => {
17271
+ const user = c.get("user");
17272
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17273
+ if (!row) return c.json({ error: "not found" }, 404);
17274
+ const t0 = Date.now();
17275
+ try {
17276
+ const page = await readPage(row.cdp_ws_url);
17277
+ await charge(row.id, user.id, t0);
17278
+ await recordAction({ sessionId: row.id, type: "read", params: null, ok: true });
17279
+ return c.json(page);
17280
+ } catch (err) {
17281
+ await charge(row.id, user.id, t0);
17282
+ return c.json(failure(err), 502);
17283
+ }
17284
+ });
17285
+ app2.post("/sessions/:id/click", async (c) => {
17286
+ const user = c.get("user");
17287
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17288
+ if (!row) return c.json({ error: "not found" }, 404);
17289
+ const body = await c.req.json().catch(() => ({}));
17290
+ const x = Number(body.x);
17291
+ const y = Number(body.y);
17292
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return c.json({ error: "x and y are required" }, 400);
17293
+ const t0 = Date.now();
17294
+ try {
17295
+ await click(row.runtime_session_id, x, y, {
17296
+ button: body.button === "right" || body.button === "middle" ? body.button : "left",
17297
+ numClicks: typeof body.num_clicks === "number" ? body.num_clicks : void 0
17298
+ });
17299
+ await charge(row.id, user.id, t0);
17300
+ await recordAction({ sessionId: row.id, type: "click", params: { x, y }, ok: true });
17301
+ return c.json({ ok: true });
17302
+ } catch (err) {
17303
+ await charge(row.id, user.id, t0);
17304
+ await recordAction({ sessionId: row.id, type: "click", params: { x, y }, ok: false, error: String(err) });
17305
+ return c.json(failure(err), 502);
17306
+ }
17307
+ });
17308
+ app2.post("/sessions/:id/type", async (c) => {
17309
+ const user = c.get("user");
17310
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17311
+ if (!row) return c.json({ error: "not found" }, 404);
17312
+ const body = await c.req.json().catch(() => ({}));
17313
+ const text = typeof body.text === "string" ? body.text : "";
17314
+ if (!text) return c.json({ error: "text is required" }, 400);
17315
+ const t0 = Date.now();
17316
+ try {
17317
+ await typeText(row.runtime_session_id, text, typeof body.delay === "number" ? body.delay : void 0);
17318
+ await charge(row.id, user.id, t0);
17319
+ await recordAction({ sessionId: row.id, type: "type", params: { length: text.length }, ok: true });
17320
+ return c.json({ ok: true });
17321
+ } catch (err) {
17322
+ await charge(row.id, user.id, t0);
17323
+ return c.json(failure(err), 502);
17324
+ }
17325
+ });
17326
+ app2.post("/sessions/:id/scroll", async (c) => {
17327
+ const user = c.get("user");
17328
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17329
+ if (!row) return c.json({ error: "not found" }, 404);
17330
+ const body = await c.req.json().catch(() => ({}));
17331
+ const x = typeof body.x === "number" ? body.x : 640;
17332
+ const y = typeof body.y === "number" ? body.y : 400;
17333
+ const deltaX = typeof body.delta_x === "number" ? body.delta_x : 0;
17334
+ const deltaY = typeof body.delta_y === "number" ? body.delta_y : 5;
17335
+ const t0 = Date.now();
17336
+ try {
17337
+ await scroll(row.runtime_session_id, x, y, deltaX, deltaY);
17338
+ await charge(row.id, user.id, t0);
17339
+ await recordAction({ sessionId: row.id, type: "scroll", params: { deltaX, deltaY }, ok: true });
17340
+ return c.json({ ok: true });
17341
+ } catch (err) {
17342
+ await charge(row.id, user.id, t0);
17343
+ return c.json(failure(err), 502);
17344
+ }
17345
+ });
17346
+ app2.post("/sessions/:id/press", async (c) => {
17347
+ const user = c.get("user");
17348
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17349
+ if (!row) return c.json({ error: "not found" }, 404);
17350
+ const body = await c.req.json().catch(() => ({}));
17351
+ const keys = Array.isArray(body.keys) ? body.keys.map(String) : [];
17352
+ if (!keys.length) return c.json({ error: "keys is required" }, 400);
17353
+ const t0 = Date.now();
17354
+ try {
17355
+ await pressKeys(row.runtime_session_id, keys);
17356
+ await charge(row.id, user.id, t0);
17357
+ await recordAction({ sessionId: row.id, type: "press", params: { keys }, ok: true });
17358
+ return c.json({ ok: true });
17359
+ } catch (err) {
17360
+ await charge(row.id, user.id, t0);
17361
+ return c.json(failure(err), 502);
17362
+ }
17363
+ });
17364
+ app2.post("/sessions/:id/replay/start", async (c) => {
17365
+ const user = c.get("user");
17366
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17367
+ if (!row) return c.json({ error: "not found" }, 404);
17368
+ const body = await c.req.json().catch(() => ({}));
17369
+ try {
17370
+ const started = await replayStart(row.runtime_session_id);
17371
+ await recordReplayStart({
17372
+ sessionId: row.id,
17373
+ replayId: started.replayId,
17374
+ viewUrl: started.viewUrl,
17375
+ label: typeof body.label === "string" ? body.label : null
17376
+ });
17377
+ return c.json({ replay_id: started.replayId });
17378
+ } catch (err) {
17379
+ return c.json(failure(err), 502);
17380
+ }
17381
+ });
17382
+ app2.post("/sessions/:id/replay/stop", async (c) => {
17383
+ const user = c.get("user");
17384
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17385
+ if (!row) return c.json({ error: "not found" }, 404);
17386
+ const body = await c.req.json().catch(() => ({}));
17387
+ const replayId = typeof body.replay_id === "string" ? body.replay_id : "";
17388
+ if (!replayId) return c.json({ error: "replay_id is required" }, 400);
17389
+ try {
17390
+ await replayStop(row.runtime_session_id, replayId);
17391
+ let viewUrl = null;
17392
+ try {
17393
+ const all = await replayList(row.runtime_session_id);
17394
+ viewUrl = all.find((r) => r.replayId === replayId)?.viewUrl ?? null;
17395
+ } catch {
17396
+ viewUrl = null;
17397
+ }
17398
+ await recordReplayStop(replayId, viewUrl);
17399
+ return c.json({ ok: true });
17400
+ } catch (err) {
17401
+ return c.json(failure(err), 502);
17402
+ }
17403
+ });
17404
+ app2.get("/sessions/:id/replays", async (c) => {
17405
+ const user = c.get("user");
17406
+ const row = await loadOpenSession(c.req.param("id"), user.id);
17407
+ if (!row) return c.json({ error: "not found" }, 404);
17408
+ const rows = await listReplayRows(row.id);
17409
+ return c.json({
17410
+ replays: rows.map((r) => ({
17411
+ replay_id: r.replay_id,
17412
+ view_url: r.view_url,
17413
+ label: r.label,
17414
+ started_at: r.started_at,
17415
+ stopped_at: r.stopped_at
17416
+ }))
17417
+ });
17418
+ });
17419
+ return app2;
17420
+ }
17421
+ var import_hono8, auth;
17422
+ var init_browser_agent_routes = __esm({
17423
+ "src/api/browser-agent-routes.ts"() {
17424
+ "use strict";
17425
+ import_hono8 = require("hono");
17426
+ init_api_auth();
17427
+ init_errors();
17428
+ init_db();
17429
+ init_rates();
17430
+ init_browser_agent_db();
17431
+ init_browser_agent_service();
17432
+ auth = createApiKeyAuth();
17433
+ }
17434
+ });
17435
+
17436
+ // src/api/browser-agent-console.ts
17437
+ function renderConsoleHtml(initialSessionId) {
17438
+ const initial = JSON.stringify(initialSessionId ?? "");
17439
+ return `<!DOCTYPE html>
17440
+ <html lang="en">
17441
+ <head>
17442
+ <meta charset="UTF-8" />
17443
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
17444
+ <title>Browser Agent Console</title>
17445
+ <style>
17446
+ :root { color-scheme: dark; }
17447
+ :where(*) { box-sizing: border-box; }
17448
+ body { margin: 0; font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif; background: #0b0e14; color: #d7dce5; }
17449
+ header { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid #1c2230; background: #0f131c; }
17450
+ header h1 { font-size: 15px; margin: 0; font-weight: 600; color: #fff; letter-spacing: .2px; }
17451
+ header .spacer { flex: 1; }
17452
+ input, button, select { font: inherit; }
17453
+ input[type=text], input[type=password], input[type=url] { background: #141925; border: 1px solid #232b3a; color: #e6eaf2; border-radius: 7px; padding: 7px 10px; }
17454
+ button { background: #2b6cff; border: 0; color: #fff; border-radius: 7px; padding: 7px 12px; cursor: pointer; font-weight: 500; }
17455
+ button.ghost { background: #1a2030; color: #cdd5e4; border: 1px solid #28303f; }
17456
+ button:disabled { opacity: .5; cursor: default; }
17457
+ .layout { display: grid; grid-template-columns: 280px 1fr; height: calc(100vh - 53px); }
17458
+ aside { border-right: 1px solid #1c2230; overflow-y: auto; padding: 12px; }
17459
+ aside h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #6b7689; margin: 4px 4px 10px; }
17460
+ .sess { padding: 9px 10px; border-radius: 8px; border: 1px solid #1c2230; margin-bottom: 8px; cursor: pointer; }
17461
+ .sess:hover { border-color: #2b6cff; }
17462
+ .sess.active { border-color: #2b6cff; background: #131b2e; }
17463
+ .sess .id { font-family: ui-monospace, monospace; font-size: 12px; color: #aeb8cc; }
17464
+ .sess .meta { font-size: 11px; color: #6b7689; margin-top: 3px; }
17465
+ .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; }
17466
+ .dot.open { background: #36d399; } .dot.closed { background: #5a6677; }
17467
+ main { display: flex; flex-direction: column; overflow: hidden; }
17468
+ .toolbar { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid #1c2230; }
17469
+ .toolbar label { font-size: 13px; color: #aeb8cc; display: flex; align-items: center; gap: 6px; }
17470
+ .stage { flex: 1; position: relative; background: #05070b; overflow: auto; }
17471
+ .stage iframe { width: 100%; height: 100%; border: 0; display: block; }
17472
+ .empty { display: flex; align-items: center; justify-content: center; height: 100%; color: #5a6677; flex-direction: column; gap: 10px; }
17473
+ .replays { border-top: 1px solid #1c2230; padding: 10px 16px; max-height: 200px; overflow-y: auto; }
17474
+ .replays h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #6b7689; margin: 0 0 8px; }
17475
+ .replay { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; }
17476
+ .replay a { color: #7aa2ff; }
17477
+ .gate { max-width: 380px; margin: 80px auto; padding: 24px; border: 1px solid #1c2230; border-radius: 12px; background: #0f131c; }
17478
+ .gate h2 { margin: 0 0 6px; font-size: 16px; color: #fff; }
17479
+ .gate p { color: #8893a7; margin: 0 0 16px; }
17480
+ .gate input { width: 100%; margin-bottom: 12px; }
17481
+ .gate button { width: 100%; }
17482
+ .muted { color: #6b7689; font-size: 12px; }
17483
+ </style>
17484
+ </head>
17485
+ <body>
17486
+ <div id="app"></div>
17487
+ <script>
17488
+ const INITIAL_SESSION = ${initial};
17489
+ const KEY_STORE = 'browser_agent_api_key';
17490
+ let state = { key: localStorage.getItem(KEY_STORE) || '', sessions: [], current: INITIAL_SESSION || null, readOnly: true, liveUrl: null, replays: [] };
17491
+
17492
+ function api(method, path, body) {
17493
+ return fetch('/agent' + path, {
17494
+ method,
17495
+ headers: { 'Content-Type': 'application/json', 'x-api-key': state.key },
17496
+ body: body ? JSON.stringify(body) : undefined,
17497
+ }).then(async r => ({ ok: r.ok, data: await r.json().catch(() => ({})) }));
17498
+ }
17499
+
17500
+ async function refreshSessions() {
17501
+ const r = await api('GET', '/sessions?all=1');
17502
+ if (r.ok) { state.sessions = r.data.sessions || []; render(); }
17503
+ }
17504
+
17505
+ async function selectSession(id) {
17506
+ state.current = id; state.liveUrl = null; state.replays = [];
17507
+ history.replaceState(null, '', '/console/' + id);
17508
+ render();
17509
+ const live = await api('GET', '/sessions/' + id + '/live-view');
17510
+ state.liveUrl = live.ok ? live.data.live_view_url : null;
17511
+ const reps = await api('GET', '/sessions/' + id + '/replays');
17512
+ state.replays = reps.ok ? (reps.data.replays || []) : [];
17513
+ render();
17514
+ }
17515
+
17516
+ async function openSession() {
17517
+ const r = await api('POST', '/sessions', { label: 'console' });
17518
+ if (r.ok) { await refreshSessions(); selectSession(r.data.session_id); }
17519
+ else alert('Open failed: ' + JSON.stringify(r.data));
17520
+ }
17521
+
17522
+ async function closeCurrent() {
17523
+ if (!state.current) return;
17524
+ await api('DELETE', '/sessions/' + state.current);
17525
+ await refreshSessions();
17526
+ }
17527
+
17528
+ function frameSrc() {
17529
+ if (!state.liveUrl) return null;
17530
+ const sep = state.liveUrl.includes('?') ? '&' : '?';
17531
+ return state.readOnly ? state.liveUrl + sep + 'readOnly=true' : state.liveUrl;
17532
+ }
17533
+
17534
+ function saveKey(v) { state.key = v.trim(); localStorage.setItem(KEY_STORE, state.key); render(); if (state.key) { refreshSessions(); if (state.current) selectSession(state.current); } }
17535
+
17536
+ function h(html) { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstChild; }
17537
+ function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
17538
+
17539
+ function render() {
17540
+ const app = document.getElementById('app');
17541
+ app.innerHTML = '';
17542
+ if (!state.key) {
17543
+ app.appendChild(h('<div class="gate"><h2>Browser Agent Console</h2><p>Paste your API key to watch and control browser sessions.</p><input id="k" type="password" placeholder="API key" /><button id="kb">Continue</button></div>'));
17544
+ document.getElementById('kb').onclick = () => saveKey(document.getElementById('k').value);
17545
+ document.getElementById('k').onkeydown = e => { if (e.key === 'Enter') saveKey(e.target.value); };
17546
+ return;
17547
+ }
17548
+ const header = h('<header><h1>Browser Agent</h1><span class="muted">live control + replays</span><span class="spacer"></span><button id="open">+ New Session</button><button class="ghost" id="logout">Forget key</button></header>');
17549
+ app.appendChild(header);
17550
+ document.getElementById('open').onclick = openSession;
17551
+ document.getElementById('logout').onclick = () => saveKey('');
17552
+
17553
+ const layout = h('<div class="layout"></div>');
17554
+ const aside = h('<aside><h2>Sessions</h2></aside>');
17555
+ if (!state.sessions.length) aside.appendChild(h('<div class="muted" style="padding:4px">No sessions yet.</div>'));
17556
+ for (const s of state.sessions) {
17557
+ const el = h('<div class="sess ' + (s.session_id === state.current ? 'active' : '') + '"><div class="id">' + esc(s.session_id) + '</div><div class="meta"><span class="dot ' + esc(s.status) + '"></span>' + esc(s.status) + (s.label ? ' \xB7 ' + esc(s.label) : '') + '</div></div>');
17558
+ el.onclick = () => selectSession(s.session_id);
17559
+ aside.appendChild(el);
17560
+ }
17561
+ layout.appendChild(aside);
17562
+
17563
+ const main = h('<main></main>');
17564
+ if (!state.current) {
17565
+ main.appendChild(h('<div class="empty"><div>Select or open a session to watch.</div></div>'));
17566
+ } else {
17567
+ const tb = h('<div class="toolbar"><label><input type="checkbox" id="ro" ' + (state.readOnly ? 'checked' : '') + ' /> Read-only (uncheck to take control)</label><span class="spacer"></span><button class="ghost" id="reload">Reload view</button><button class="ghost" id="close">Close session</button></div>');
17568
+ main.appendChild(tb);
17569
+ const stage = h('<div class="stage"></div>');
17570
+ const src = frameSrc();
17571
+ if (src) {
17572
+ const f = h('<iframe allow="autoplay; clipboard-read; clipboard-write" src="' + esc(src) + '"></iframe>');
17573
+ stage.appendChild(f);
17574
+ } else {
17575
+ stage.appendChild(h('<div class="empty"><div>Live view unavailable for this session.</div><div class="muted">It may be closed or still starting.</div></div>'));
17576
+ }
17577
+ main.appendChild(stage);
17578
+
17579
+ const rep = h('<div class="replays"><h3>Replays</h3></div>');
17580
+ if (!state.replays.length) rep.appendChild(h('<div class="muted">No replays recorded.</div>'));
17581
+ for (const r of state.replays) {
17582
+ const status = r.stopped_at ? 'ready' : 'recording\u2026';
17583
+ const link = r.view_url ? '<a href="' + esc(r.view_url) + '" target="_blank" rel="noopener">view mp4</a>' : '<span class="muted">' + status + '</span>';
17584
+ rep.appendChild(h('<div class="replay"><span class="muted">' + esc(r.started_at || '') + '</span><span class="spacer"></span>' + link + '</div>'));
17585
+ }
17586
+ main.appendChild(rep);
17587
+
17588
+ layout.appendChild(main);
17589
+ }
17590
+ app.appendChild(layout);
17591
+
17592
+ const ro = document.getElementById('ro');
17593
+ if (ro) ro.onchange = e => { state.readOnly = e.target.checked; render(); };
17594
+ const reload = document.getElementById('reload');
17595
+ if (reload) reload.onclick = () => selectSession(state.current);
17596
+ const close = document.getElementById('close');
17597
+ if (close) close.onclick = closeCurrent;
17598
+ }
17599
+
17600
+ render();
17601
+ if (state.key) { refreshSessions(); if (state.current) selectSession(state.current); }
17602
+ </script>
17603
+ </body>
17604
+ </html>`;
17605
+ }
17606
+ var init_browser_agent_console = __esm({
17607
+ "src/api/browser-agent-console.ts"() {
17608
+ "use strict";
17609
+ }
17610
+ });
17611
+
16812
17612
  // src/api/stripe-routes.ts
16813
- var import_stripe, import_hono8, stripe, stripeApp;
17613
+ var import_stripe, import_hono9, stripe, stripeApp;
16814
17614
  var init_stripe_routes = __esm({
16815
17615
  "src/api/stripe-routes.ts"() {
16816
17616
  "use strict";
16817
17617
  import_stripe = __toESM(require("stripe"), 1);
16818
- import_hono8 = require("hono");
17618
+ import_hono9 = require("hono");
16819
17619
  init_db();
16820
17620
  init_rates();
16821
17621
  stripe = new import_stripe.default(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
16822
- stripeApp = new import_hono8.Hono();
17622
+ stripeApp = new import_hono9.Hono();
16823
17623
  stripeApp.post("/webhooks", async (c) => {
16824
17624
  const sig = c.req.header("stripe-signature");
16825
17625
  const body = await c.req.text();
@@ -17106,14 +17906,14 @@ function getSessionSecret() {
17106
17906
  function safeEqualHex(a, b) {
17107
17907
  if (a.length !== b.length) return false;
17108
17908
  try {
17109
- return (0, import_node_crypto3.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
17909
+ return (0, import_node_crypto4.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
17110
17910
  } catch {
17111
17911
  return false;
17112
17912
  }
17113
17913
  }
17114
17914
  function signSession(userId) {
17115
17915
  const payload = String(userId);
17116
- const sig = (0, import_node_crypto3.createHmac)("sha256", secret()).update(payload).digest("hex");
17916
+ const sig = (0, import_node_crypto4.createHmac)("sha256", secret()).update(payload).digest("hex");
17117
17917
  return `${payload}.${sig}`;
17118
17918
  }
17119
17919
  function verifySession(token) {
@@ -17121,16 +17921,16 @@ function verifySession(token) {
17121
17921
  if (dot === -1) return null;
17122
17922
  const payload = token.slice(0, dot);
17123
17923
  const sig = token.slice(dot + 1);
17124
- const expected = (0, import_node_crypto3.createHmac)("sha256", secret()).update(payload).digest("hex");
17924
+ const expected = (0, import_node_crypto4.createHmac)("sha256", secret()).update(payload).digest("hex");
17125
17925
  if (!safeEqualHex(sig, expected)) return null;
17126
17926
  const id = parseInt(payload);
17127
17927
  return isNaN(id) ? null : id;
17128
17928
  }
17129
- var import_node_crypto3, isProduction, secret;
17929
+ var import_node_crypto4, isProduction, secret;
17130
17930
  var init_session = __esm({
17131
17931
  "src/api/session.ts"() {
17132
17932
  "use strict";
17133
- import_node_crypto3 = require("crypto");
17933
+ import_node_crypto4 = require("crypto");
17134
17934
  isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
17135
17935
  secret = () => getSessionSecret();
17136
17936
  }
@@ -17346,7 +18146,7 @@ async function checkHarvestLimits(userId, email, extraSlots = 0) {
17346
18146
  if (active >= limit) return { error: `You have ${active} job${active !== 1 ? "s" : ""} running. Your account allows ${limit} concurrent job${limit !== 1 ? "s" : ""}. Wait for one to finish or add a concurrency slot at mcpscraper.dev/billing.` };
17347
18147
  return null;
17348
18148
  }
17349
- var import_resend, import_hono9, import_hono10, import_factory6, import_cookie, import_stripe2, secureCookies, isProduction2, sessionCookieOptions, requireAllowedOrigin, auth, adminAuth, sessionAuth, app, STRIPE_API_VERSION, BYPASS_EMAILS, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
18149
+ var import_resend, import_hono10, import_hono11, import_factory6, import_cookie, import_stripe2, secureCookies, isProduction2, sessionCookieOptions, requireAllowedOrigin, auth2, adminAuth, sessionAuth, app, STRIPE_API_VERSION, BYPASS_EMAILS, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
17350
18150
  var init_server = __esm({
17351
18151
  "src/api/server.ts"() {
17352
18152
  "use strict";
@@ -17363,8 +18163,8 @@ var init_server = __esm({
17363
18163
  init_media_extractor();
17364
18164
  init_site_mapper();
17365
18165
  init_site_extractor();
17366
- import_hono9 = require("hono");
17367
- import_hono10 = require("inngest/hono");
18166
+ import_hono10 = require("hono");
18167
+ import_hono11 = require("inngest/hono");
17368
18168
  init_client();
17369
18169
  init_site_audit();
17370
18170
  init_site_audit_routes();
@@ -17374,6 +18174,8 @@ var init_server = __esm({
17374
18174
  init_maps_routes();
17375
18175
  init_serp_intelligence_routes();
17376
18176
  init_mcp_routes();
18177
+ init_browser_agent_routes();
18178
+ init_browser_agent_console();
17377
18179
  init_stripe_routes();
17378
18180
  init_site_audit_worker();
17379
18181
  import_factory6 = require("hono/factory");
@@ -17403,7 +18205,7 @@ var init_server = __esm({
17403
18205
  if (!configuredOrigins().has(origin)) return c.json({ error: "Origin not allowed" }, 403);
17404
18206
  return next();
17405
18207
  });
17406
- auth = (0, import_factory6.createMiddleware)(async (c, next) => {
18208
+ auth2 = (0, import_factory6.createMiddleware)(async (c, next) => {
17407
18209
  const key = c.req.header("x-api-key");
17408
18210
  if (!key) return c.json({ error: "Missing API key" }, 401);
17409
18211
  const user = await getUserByApiKey(key);
@@ -17432,7 +18234,7 @@ var init_server = __esm({
17432
18234
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
17433
18235
  return next();
17434
18236
  });
17435
- app = new import_hono9.Hono();
18237
+ app = new import_hono10.Hono();
17436
18238
  STRIPE_API_VERSION = "2026-02-25.clover";
17437
18239
  app.use("*", async (c, next) => {
17438
18240
  await next();
@@ -17592,7 +18394,7 @@ var init_server = __esm({
17592
18394
  const parsed = raw === void 0 ? NaN : Number(raw);
17593
18395
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
17594
18396
  })();
17595
- app.post("/harvest", auth, async (c) => {
18397
+ app.post("/harvest", auth2, async (c) => {
17596
18398
  const user = c.get("user");
17597
18399
  const raw = await c.req.json().catch(() => ({}));
17598
18400
  const bodyResult = HarvestBodySchema.safeParse(raw);
@@ -17634,7 +18436,7 @@ var init_server = __esm({
17634
18436
  }
17635
18437
  return c.json({ job_id: jobId, status: "pending" }, 202);
17636
18438
  });
17637
- app.post("/harvest/sync", auth, async (c) => {
18439
+ app.post("/harvest/sync", auth2, async (c) => {
17638
18440
  const user = c.get("user");
17639
18441
  const raw = await c.req.json().catch(() => ({}));
17640
18442
  const bodyResult = HarvestBodySchema.safeParse(raw);
@@ -17699,17 +18501,17 @@ var init_server = __esm({
17699
18501
  return c.json({ job_id: jobId, status: "failed", ...response, attempts: sanitizeAttempts(attempts) }, problem.httpStatus);
17700
18502
  }
17701
18503
  });
17702
- app.get("/jobs/:id", auth, async (c) => {
18504
+ app.get("/jobs/:id", auth2, async (c) => {
17703
18505
  const job = await getJob(c.req.param("id"), c.get("user").id);
17704
18506
  if (!job) return c.json({ error: "Job not found" }, 404);
17705
18507
  const attempts = await listHarvestAttempts(job.id, c.get("user").id);
17706
18508
  const safeResult = job.result && typeof job.result === "object" ? sanitizeHarvestResult(job.result) : job.result;
17707
18509
  return c.json({ ...job, result: safeResult, attempts: sanitizeAttempts(attempts) });
17708
18510
  });
17709
- app.get("/jobs", auth, async (c) => {
18511
+ app.get("/jobs", auth2, async (c) => {
17710
18512
  return c.json(await listJobs(c.get("user").id));
17711
18513
  });
17712
- app.get("/history", auth, async (c) => {
18514
+ app.get("/history", auth2, async (c) => {
17713
18515
  const userId = c.get("user").id;
17714
18516
  const [jobs, events] = await Promise.all([
17715
18517
  listJobs(userId),
@@ -17739,7 +18541,7 @@ var init_server = __esm({
17739
18541
  const rows = [...jobRows, ...eventRows].sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
17740
18542
  return c.json(rows.slice(0, 100));
17741
18543
  });
17742
- app.get("/ledger", auth, async (c) => {
18544
+ app.get("/ledger", auth2, async (c) => {
17743
18545
  return c.json(await getLedger(c.get("user").id, 100));
17744
18546
  });
17745
18547
  app.post("/admin/users", adminAuth, async (c) => {
@@ -17777,11 +18579,11 @@ var init_server = __esm({
17777
18579
  }
17778
18580
  return c.json({ processed, credited, skipped, users_credited });
17779
18581
  });
17780
- app.post("/extract-url", auth, async (c) => {
18582
+ app.post("/extract-url", auth2, async (c) => {
17781
18583
  const raw = await c.req.json().catch(() => ({}));
17782
18584
  const bodyResult = ExtractUrlBodySchema.safeParse(raw);
17783
18585
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
17784
- const { url, screenshot, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
18586
+ const { url, screenshot: screenshot2, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
17785
18587
  if (!allowLocal) {
17786
18588
  const checked = await validatePublicHttpUrl(url, { field: "URL" });
17787
18589
  if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
@@ -17807,7 +18609,7 @@ var init_server = __esm({
17807
18609
  const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
17808
18610
  const [result, pageData] = await Promise.all([
17809
18611
  extractKpo({ url: canonicalUrl, kernelApiKey }),
17810
- screenshot || extractBranding ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot, branding: !!extractBranding }).catch(() => null) : null
18612
+ screenshot2 || extractBranding ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding }).catch(() => null) : null
17811
18613
  ]);
17812
18614
  const screenshotBuf = pageData?.screenshot ?? null;
17813
18615
  const brandingData = pageData?.branding ?? null;
@@ -17825,7 +18627,7 @@ var init_server = __esm({
17825
18627
  return c.json({ error: msg }, 500);
17826
18628
  }
17827
18629
  });
17828
- app.post("/map-urls", auth, async (c) => {
18630
+ app.post("/map-urls", auth2, async (c) => {
17829
18631
  const raw = await c.req.json().catch(() => ({}));
17830
18632
  const bodyResult = MapUrlsBodySchema.safeParse(raw);
17831
18633
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -17865,7 +18667,7 @@ var init_server = __esm({
17865
18667
  return c.json({ error: msg }, 500);
17866
18668
  }
17867
18669
  });
17868
- app.post("/extract-site", auth, async (c) => {
18670
+ app.post("/extract-site", auth2, async (c) => {
17869
18671
  const raw = await c.req.json().catch(() => ({}));
17870
18672
  const bodyResult = ExtractSiteBodySchema.safeParse(raw);
17871
18673
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
@@ -17987,7 +18789,7 @@ var init_server = __esm({
17987
18789
  await setConcurrencySubId(user.id, null);
17988
18790
  return c.json({ ok: true, concurrency_limit: user.extra_concurrency_slots });
17989
18791
  });
17990
- app.get("/billing/balance", auth, async (c) => {
18792
+ app.get("/billing/balance", auth2, async (c) => {
17991
18793
  const user = c.get("user");
17992
18794
  const balanceMc = await reconcileBalanceMc(user.id);
17993
18795
  const ledger = await getLedger(user.id, 20);
@@ -17999,7 +18801,7 @@ var init_server = __esm({
17999
18801
  ledger
18000
18802
  });
18001
18803
  });
18002
- app.post("/billing/credits", auth, async (c) => {
18804
+ app.post("/billing/credits", auth2, async (c) => {
18003
18805
  const user = c.get("user");
18004
18806
  const balanceMc = await reconcileBalanceMc(user.id);
18005
18807
  const body = await c.req.json().catch(() => ({}));
@@ -18036,7 +18838,7 @@ var init_server = __esm({
18036
18838
  ]);
18037
18839
  return c.json({ drained: results.length, results, sweepResult });
18038
18840
  });
18039
- app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono10.serve)({ client: inngest, functions: [siteAuditFn] }));
18841
+ app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono11.serve)({ client: inngest, functions: [siteAuditFn] }));
18040
18842
  app.route("/api/internal/site-architecture-auditor", siteAuditApp);
18041
18843
  app.route("/youtube", youtubeApp);
18042
18844
  app.route("/screenshot", screenshotApp);
@@ -18044,6 +18846,9 @@ var init_server = __esm({
18044
18846
  app.route("/maps", mapsApp);
18045
18847
  app.route("/serp-intelligence", serpIntelligenceApp);
18046
18848
  app.route("/mcp", mcpApp);
18849
+ app.route("/agent", buildBrowserAgentRoutes());
18850
+ app.get("/console", (c) => c.html(renderConsoleHtml()));
18851
+ app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
18047
18852
  app.route("/stripe", stripeApp);
18048
18853
  if (!process.env.INNGEST_EVENT_KEY) {
18049
18854
  startSiteAuditWorker();