@hasna/browser 0.1.0 → 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.
package/dist/mcp/index.js CHANGED
@@ -324,6 +324,54 @@ function runMigrations(db) {
324
324
  CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
325
325
  CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
326
326
  `
327
+ },
328
+ {
329
+ version: 7,
330
+ sql: `
331
+ CREATE TABLE IF NOT EXISTS workflows (
332
+ id TEXT PRIMARY KEY,
333
+ name TEXT NOT NULL UNIQUE,
334
+ description TEXT,
335
+ steps TEXT NOT NULL DEFAULT '[]',
336
+ start_url TEXT,
337
+ last_run TEXT,
338
+ last_heal TEXT,
339
+ heal_count INTEGER DEFAULT 0,
340
+ run_count INTEGER DEFAULT 0,
341
+ created_at TEXT DEFAULT (datetime('now')),
342
+ updated_at TEXT DEFAULT (datetime('now'))
343
+ );
344
+ `
345
+ },
346
+ {
347
+ version: 8,
348
+ sql: `
349
+ CREATE TABLE IF NOT EXISTS datasets (
350
+ id TEXT PRIMARY KEY,
351
+ name TEXT NOT NULL UNIQUE,
352
+ source_url TEXT,
353
+ source_type TEXT NOT NULL DEFAULT 'page',
354
+ data TEXT NOT NULL DEFAULT '[]',
355
+ schema TEXT,
356
+ row_count INTEGER DEFAULT 0,
357
+ last_refresh TEXT,
358
+ created_at TEXT DEFAULT (datetime('now')),
359
+ updated_at TEXT DEFAULT (datetime('now'))
360
+ );
361
+
362
+ CREATE TABLE IF NOT EXISTS api_endpoints (
363
+ id TEXT PRIMARY KEY,
364
+ session_id TEXT,
365
+ url TEXT NOT NULL,
366
+ method TEXT DEFAULT 'GET',
367
+ response_schema TEXT,
368
+ sample_response TEXT,
369
+ status_code INTEGER,
370
+ content_type TEXT,
371
+ discovered_at TEXT DEFAULT (datetime('now'))
372
+ );
373
+ CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
374
+ `
327
375
  }
328
376
  ];
329
377
  for (const m of migrations) {
@@ -1284,14 +1332,14 @@ function enableConsoleCapture(page, sessionId) {
1284
1332
  warning: "warn"
1285
1333
  };
1286
1334
  const level = levelMap[msg.type()] ?? "log";
1287
- const location = msg.location();
1335
+ const location2 = msg.location();
1288
1336
  try {
1289
1337
  logConsoleMessage({
1290
1338
  session_id: sessionId,
1291
1339
  level,
1292
1340
  message: msg.text(),
1293
- source: location.url || undefined,
1294
- line_number: location.lineNumber || undefined
1341
+ source: location2.url || undefined,
1342
+ line_number: location2.lineNumber || undefined
1295
1343
  });
1296
1344
  } catch {}
1297
1345
  };
@@ -9954,6 +10002,419 @@ var init_annotate = __esm(() => {
9954
10002
  import_sharp3 = __toESM(require_lib(), 1);
9955
10003
  });
9956
10004
 
10005
+ // src/lib/env-detector.ts
10006
+ var exports_env_detector = {};
10007
+ __export(exports_env_detector, {
10008
+ detectEnvironment: () => detectEnvironment
10009
+ });
10010
+ async function detectEnvironment(page) {
10011
+ const url = page.url();
10012
+ const signals = [];
10013
+ let score = { local: 0, dev: 0, staging: 0, prod: 0 };
10014
+ try {
10015
+ const u = new URL(url);
10016
+ if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "0.0.0.0" || u.hostname.endsWith(".local")) {
10017
+ score.local += 5;
10018
+ signals.push(`URL hostname: ${u.hostname} \u2192 local`);
10019
+ } else if (u.hostname.match(/^(dev|development)\./i) || u.port !== "") {
10020
+ score.dev += 4;
10021
+ signals.push(`URL pattern: ${u.hostname}:${u.port} \u2192 dev`);
10022
+ } else if (u.hostname.match(/^(staging|stg|stage|preprod|uat)\./i)) {
10023
+ score.staging += 4;
10024
+ signals.push(`URL pattern: ${u.hostname} \u2192 staging`);
10025
+ } else {
10026
+ score.prod += 2;
10027
+ signals.push(`URL looks production: ${u.hostname}`);
10028
+ }
10029
+ if (u.port && !["80", "443", ""].includes(u.port)) {
10030
+ score.dev += 2;
10031
+ signals.push(`Non-standard port: ${u.port}`);
10032
+ }
10033
+ if (u.protocol === "https:") {
10034
+ score.prod += 1;
10035
+ signals.push("HTTPS \u2192 likely prod");
10036
+ } else {
10037
+ score.dev += 2;
10038
+ signals.push("HTTP \u2192 likely dev/local");
10039
+ }
10040
+ } catch {}
10041
+ try {
10042
+ const pageSignals = await page.evaluate(() => {
10043
+ const s = [];
10044
+ const w = window;
10045
+ const envVars = ["__ENV__", "__NEXT_DATA__", "__NUXT__", "process"];
10046
+ for (const v of envVars) {
10047
+ if (w[v]) {
10048
+ const env2 = w[v]?.env?.NODE_ENV ?? w[v]?.runtimeConfig?.public?.env ?? w[v]?.props?.pageProps?.env;
10049
+ if (env2)
10050
+ s.push(`window.${v}: ${env2}`);
10051
+ }
10052
+ }
10053
+ if (w.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.size > 0) {
10054
+ const fiber = document.querySelector("[data-reactroot]") || document.getElementById("__next") || document.getElementById("root");
10055
+ if (fiber)
10056
+ s.push("React app detected");
10057
+ }
10058
+ const envMeta = document.querySelector('meta[name="environment"], meta[name="env"], meta[name="deploy-env"]');
10059
+ if (envMeta)
10060
+ s.push(`meta[environment]: ${envMeta.getAttribute("content")}`);
10061
+ const scripts = document.querySelectorAll("script[src]");
10062
+ let minified = 0, unminified = 0;
10063
+ scripts.forEach((s2) => {
10064
+ const src = s2.getAttribute("src") ?? "";
10065
+ if (src.includes(".min.") || src.match(/\.[a-f0-9]{8,}\./))
10066
+ minified++;
10067
+ else if (src.endsWith(".js") && !src.includes("chunk"))
10068
+ unminified++;
10069
+ });
10070
+ if (unminified > minified && unminified > 2)
10071
+ s.push(`Unminified scripts (${unminified}/${minified + unminified}) \u2192 likely dev`);
10072
+ else if (minified > 0)
10073
+ s.push(`Minified/hashed scripts (${minified}/${minified + unminified}) \u2192 likely prod`);
10074
+ if (document.querySelector("[data-testid]"))
10075
+ s.push("data-testid attributes present \u2192 dev/staging");
10076
+ if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
10077
+ s.push("Service worker active \u2192 likely prod");
10078
+ }
10079
+ if (w.Sentry)
10080
+ s.push("Sentry SDK loaded \u2192 prod monitoring");
10081
+ if (w.__DATADOG_SYNTHETICS_INLINED_SCRIPT)
10082
+ s.push("Datadog loaded \u2192 prod monitoring");
10083
+ if (w.LogRocket)
10084
+ s.push("LogRocket loaded \u2192 prod monitoring");
10085
+ if (w._lr_loaded)
10086
+ s.push("LogRocket loaded \u2192 prod monitoring");
10087
+ if (w.gtag || w.ga)
10088
+ s.push("Google Analytics loaded \u2192 likely prod");
10089
+ if (w.posthog || w._ph)
10090
+ s.push("PostHog loaded \u2192 prod analytics");
10091
+ if (w.mixpanel)
10092
+ s.push("Mixpanel loaded \u2192 prod analytics");
10093
+ const robots = document.querySelector('meta[name="robots"]');
10094
+ if (robots) {
10095
+ const content = robots.getAttribute("content") ?? "";
10096
+ if (content.includes("noindex"))
10097
+ s.push(`robots: noindex \u2192 staging/dev`);
10098
+ }
10099
+ return s;
10100
+ });
10101
+ for (const signal of pageSignals) {
10102
+ signals.push(signal);
10103
+ if (signal.includes("development") || signal.includes("\u2192 dev") || signal.includes("\u2192 likely dev"))
10104
+ score.dev += 2;
10105
+ if (signal.includes("production") || signal.includes("\u2192 prod") || signal.includes("\u2192 likely prod"))
10106
+ score.prod += 2;
10107
+ if (signal.includes("staging") || signal.includes("\u2192 staging"))
10108
+ score.staging += 2;
10109
+ if (signal.includes("monitoring") || signal.includes("analytics"))
10110
+ score.prod += 1;
10111
+ if (signal.includes("noindex")) {
10112
+ score.staging += 2;
10113
+ score.dev += 1;
10114
+ }
10115
+ }
10116
+ } catch {}
10117
+ const entries = Object.entries(score);
10118
+ entries.sort((a, b) => b[1] - a[1]);
10119
+ const [env, topScore] = entries[0];
10120
+ const [, secondScore] = entries[1];
10121
+ const confidence = topScore >= 5 ? "high" : topScore > secondScore + 1 ? "medium" : "low";
10122
+ return { env, confidence, signals };
10123
+ }
10124
+
10125
+ // src/lib/deep-performance.ts
10126
+ var exports_deep_performance = {};
10127
+ __export(exports_deep_performance, {
10128
+ getDeepPerformance: () => getDeepPerformance
10129
+ });
10130
+ function categorizeThirdParty(domain) {
10131
+ for (const [pattern, category] of Object.entries(THIRD_PARTY_CATEGORIES)) {
10132
+ if (domain.includes(pattern))
10133
+ return category;
10134
+ }
10135
+ return "other";
10136
+ }
10137
+ async function getDeepPerformance(page) {
10138
+ return page.evaluate(() => {
10139
+ const perf = performance;
10140
+ const entries = perf.getEntriesByType("resource");
10141
+ const navEntry = perf.getEntriesByType("navigation")[0];
10142
+ const paintEntries = perf.getEntriesByType("paint");
10143
+ const fcp = paintEntries.find((e) => e.name === "first-contentful-paint")?.startTime;
10144
+ const ttfb = navEntry?.responseStart;
10145
+ const web_vitals = { fcp, ttfb };
10146
+ try {
10147
+ const lcpEntries = perf.getEntriesByType("largest-contentful-paint");
10148
+ if (lcpEntries.length > 0)
10149
+ web_vitals.lcp = lcpEntries[lcpEntries.length - 1].startTime;
10150
+ } catch {}
10151
+ const byType = {};
10152
+ let totalBytes = 0;
10153
+ const resourceList = [];
10154
+ const pageDomain = location.hostname;
10155
+ const thirdPartyMap = new Map;
10156
+ for (const entry of entries) {
10157
+ const size = entry.transferSize || entry.encodedBodySize || 0;
10158
+ totalBytes += size;
10159
+ let type2 = entry.initiatorType || "other";
10160
+ if (type2 === "xmlhttprequest" || type2 === "fetch")
10161
+ type2 = "xhr";
10162
+ if (type2 === "link" && entry.name.match(/\.css/))
10163
+ type2 = "css";
10164
+ if (type2 === "img" || entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp|avif|ico)/i))
10165
+ type2 = "image";
10166
+ if (type2 === "script" || entry.name.match(/\.js/))
10167
+ type2 = "script";
10168
+ if (entry.name.match(/\.(woff2?|ttf|otf|eot)/i))
10169
+ type2 = "font";
10170
+ if (!byType[type2])
10171
+ byType[type2] = { count: 0, size_bytes: 0 };
10172
+ byType[type2].count++;
10173
+ byType[type2].size_bytes += size;
10174
+ resourceList.push({ url: entry.name, size_bytes: size, type: type2 });
10175
+ try {
10176
+ const domain = new URL(entry.name).hostname;
10177
+ if (domain !== pageDomain && !domain.endsWith(`.${pageDomain}`)) {
10178
+ if (!thirdPartyMap.has(domain))
10179
+ thirdPartyMap.set(domain, { scripts: 0, total_bytes: 0 });
10180
+ const tp = thirdPartyMap.get(domain);
10181
+ tp.scripts++;
10182
+ tp.total_bytes += size;
10183
+ }
10184
+ } catch {}
10185
+ }
10186
+ resourceList.sort((a, b) => b.size_bytes - a.size_bytes);
10187
+ const largest = resourceList.slice(0, 10).map((r) => ({
10188
+ url: r.url.length > 120 ? r.url.slice(0, 117) + "..." : r.url,
10189
+ size_bytes: r.size_bytes,
10190
+ type: r.type
10191
+ }));
10192
+ const third_party = Array.from(thirdPartyMap.entries()).map(([domain, data]) => ({ domain, ...data, category: "" })).sort((a, b) => b.total_bytes - a.total_bytes).slice(0, 15);
10193
+ const allNodes = document.querySelectorAll("*");
10194
+ let maxDepth = 0;
10195
+ let textNodes = 0;
10196
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ALL);
10197
+ let node = walker.currentNode;
10198
+ while (node) {
10199
+ if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim())
10200
+ textNodes++;
10201
+ let depth = 0;
10202
+ let parent = node.parentNode;
10203
+ while (parent) {
10204
+ depth++;
10205
+ parent = parent.parentNode;
10206
+ }
10207
+ if (depth > maxDepth)
10208
+ maxDepth = depth;
10209
+ node = walker.nextNode();
10210
+ }
10211
+ const mem = performance.memory;
10212
+ const memory = {
10213
+ js_heap_used_mb: mem ? Math.round(mem.usedJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
10214
+ js_heap_total_mb: mem ? Math.round(mem.totalJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
10215
+ js_heap_limit_mb: mem ? Math.round(mem.jsHeapSizeLimit / 1024 / 1024 * 100) / 100 : 0
10216
+ };
10217
+ return {
10218
+ web_vitals,
10219
+ resources: { total_transfer_bytes: totalBytes, total_resources: entries.length, by_type: byType, largest },
10220
+ third_party,
10221
+ dom: { node_count: document.all.length, max_depth: maxDepth, element_count: allNodes.length, text_node_count: textNodes },
10222
+ main_thread: { long_tasks: 0, total_blocking_ms: 0 },
10223
+ memory
10224
+ };
10225
+ }).then((result) => {
10226
+ for (const tp of result.third_party) {
10227
+ tp.category = categorizeThirdParty(tp.domain);
10228
+ }
10229
+ return result;
10230
+ });
10231
+ }
10232
+ var THIRD_PARTY_CATEGORIES;
10233
+ var init_deep_performance = __esm(() => {
10234
+ THIRD_PARTY_CATEGORIES = {
10235
+ "google-analytics.com": "analytics",
10236
+ "googletagmanager.com": "analytics",
10237
+ gtag: "analytics",
10238
+ "facebook.net": "social",
10239
+ "connect.facebook": "social",
10240
+ "stripe.com": "payment",
10241
+ "js.stripe.com": "payment",
10242
+ "sentry.io": "monitoring",
10243
+ "sentry-cdn": "monitoring",
10244
+ "posthog.com": "analytics",
10245
+ "ph.": "analytics",
10246
+ "intercom.io": "chat",
10247
+ "crisp.chat": "chat",
10248
+ "hotjar.com": "analytics",
10249
+ "clarity.ms": "analytics",
10250
+ "cdn.jsdelivr.net": "cdn",
10251
+ "cdnjs.cloudflare.com": "cdn",
10252
+ "unpkg.com": "cdn",
10253
+ "fonts.googleapis.com": "fonts",
10254
+ "fonts.gstatic.com": "fonts"
10255
+ };
10256
+ });
10257
+
10258
+ // src/lib/workflows.ts
10259
+ var exports_workflows = {};
10260
+ __export(exports_workflows, {
10261
+ saveWorkflowFromRecording: () => saveWorkflowFromRecording,
10262
+ saveWorkflow: () => saveWorkflow,
10263
+ runWorkflow: () => runWorkflow,
10264
+ listWorkflows: () => listWorkflows,
10265
+ getWorkflowByName: () => getWorkflowByName,
10266
+ getWorkflow: () => getWorkflow,
10267
+ deleteWorkflow: () => deleteWorkflow
10268
+ });
10269
+ import { randomUUID as randomUUID10 } from "crypto";
10270
+ function saveWorkflow(data) {
10271
+ const db = getDatabase();
10272
+ const id = randomUUID10();
10273
+ db.prepare("INSERT OR REPLACE INTO workflows (id, name, description, steps, start_url) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.description ?? null, JSON.stringify(data.steps), data.startUrl ?? null);
10274
+ return getWorkflow(id);
10275
+ }
10276
+ function saveWorkflowFromRecording(recordingId, name, description) {
10277
+ const db = getDatabase();
10278
+ const rec = db.query("SELECT steps, start_url FROM recordings WHERE id = ?").get(recordingId);
10279
+ if (!rec)
10280
+ throw new Error(`Recording not found: ${recordingId}`);
10281
+ const steps = JSON.parse(rec.steps);
10282
+ return saveWorkflow({ name, description, steps, startUrl: rec.start_url ?? undefined });
10283
+ }
10284
+ function getWorkflow(id) {
10285
+ const db = getDatabase();
10286
+ const row = db.query("SELECT * FROM workflows WHERE id = ?").get(id);
10287
+ if (!row)
10288
+ return null;
10289
+ return { ...row, steps: JSON.parse(row.steps) };
10290
+ }
10291
+ function getWorkflowByName(name) {
10292
+ const db = getDatabase();
10293
+ const row = db.query("SELECT * FROM workflows WHERE name = ?").get(name);
10294
+ if (!row)
10295
+ return null;
10296
+ return { ...row, steps: JSON.parse(row.steps) };
10297
+ }
10298
+ function listWorkflows() {
10299
+ const db = getDatabase();
10300
+ return db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all().map((row) => ({ ...row, steps: JSON.parse(row.steps) }));
10301
+ }
10302
+ function deleteWorkflow(name) {
10303
+ const db = getDatabase();
10304
+ return db.prepare("DELETE FROM workflows WHERE name = ?").run(name).changes > 0;
10305
+ }
10306
+ function recordRun(id, healed) {
10307
+ const db = getDatabase();
10308
+ if (healed) {
10309
+ db.prepare("UPDATE workflows SET last_run = datetime('now'), last_heal = datetime('now'), heal_count = heal_count + 1, run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
10310
+ } else {
10311
+ db.prepare("UPDATE workflows SET last_run = datetime('now'), run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
10312
+ }
10313
+ }
10314
+ async function runWorkflow(workflow, page) {
10315
+ const t0 = Date.now();
10316
+ let executed = 0;
10317
+ let failed = 0;
10318
+ let healed = 0;
10319
+ const healedDetails = [];
10320
+ const errors2 = [];
10321
+ const updatedSteps = [...workflow.steps];
10322
+ for (let i = 0;i < workflow.steps.length; i++) {
10323
+ const step = workflow.steps[i];
10324
+ try {
10325
+ switch (step.type) {
10326
+ case "navigate":
10327
+ if (step.url)
10328
+ await page.goto(step.url, { waitUntil: "domcontentloaded", timeout: 30000 });
10329
+ break;
10330
+ case "click":
10331
+ if (step.selector) {
10332
+ try {
10333
+ await page.click(step.selector, { timeout: 5000 });
10334
+ } catch {
10335
+ const result = await healSelector(page, step.selector);
10336
+ if (result.found && result.locator) {
10337
+ await result.locator.click();
10338
+ healed++;
10339
+ const healedSelector = `[healed:${result.method}]${step.selector}`;
10340
+ healedDetails.push({ step: i, original: step.selector, healed_to: healedSelector, method: result.method });
10341
+ } else {
10342
+ throw new Error(`Click failed: ${step.selector} (self-healing exhausted)`);
10343
+ }
10344
+ }
10345
+ }
10346
+ break;
10347
+ case "type":
10348
+ if (step.selector && step.value) {
10349
+ try {
10350
+ await page.fill(step.selector, step.value);
10351
+ } catch {
10352
+ const result = await healSelector(page, step.selector);
10353
+ if (result.found && result.locator) {
10354
+ await result.locator.fill(step.value);
10355
+ healed++;
10356
+ healedDetails.push({ step: i, original: step.selector, healed_to: `[healed:${result.method}]`, method: result.method });
10357
+ } else {
10358
+ throw new Error(`Type failed: ${step.selector} (self-healing exhausted)`);
10359
+ }
10360
+ }
10361
+ }
10362
+ break;
10363
+ case "scroll":
10364
+ if (step.y)
10365
+ await page.mouse.wheel(0, step.y);
10366
+ break;
10367
+ case "hover":
10368
+ if (step.selector) {
10369
+ try {
10370
+ await page.hover(step.selector);
10371
+ } catch {}
10372
+ }
10373
+ break;
10374
+ case "select":
10375
+ if (step.selector && step.value) {
10376
+ try {
10377
+ await page.selectOption(step.selector, step.value);
10378
+ } catch {}
10379
+ }
10380
+ break;
10381
+ case "wait":
10382
+ if (step.selector) {
10383
+ try {
10384
+ await page.waitForSelector(step.selector, { timeout: 1e4 });
10385
+ } catch {}
10386
+ } else {
10387
+ await new Promise((r) => setTimeout(r, step.timestamp || 1000));
10388
+ }
10389
+ break;
10390
+ case "evaluate":
10391
+ if (step.value)
10392
+ await page.evaluate(step.value);
10393
+ break;
10394
+ default:
10395
+ break;
10396
+ }
10397
+ executed++;
10398
+ } catch (err) {
10399
+ failed++;
10400
+ errors2.push(`Step ${i} (${step.type}): ${err instanceof Error ? err.message : String(err)}`);
10401
+ }
10402
+ }
10403
+ recordRun(workflow.id, healed > 0);
10404
+ return {
10405
+ success: failed === 0,
10406
+ steps_executed: executed,
10407
+ steps_failed: failed,
10408
+ steps_healed: healed,
10409
+ healed_details: healedDetails,
10410
+ errors: errors2,
10411
+ duration_ms: Date.now() - t0
10412
+ };
10413
+ }
10414
+ var init_workflows = __esm(() => {
10415
+ init_schema();
10416
+ });
10417
+
9957
10418
  // src/lib/auth-flow.ts
9958
10419
  var exports_auth_flow = {};
9959
10420
  __export(exports_auth_flow, {
@@ -9968,10 +10429,10 @@ __export(exports_auth_flow, {
9968
10429
  getAuthFlow: () => getAuthFlow,
9969
10430
  deleteAuthFlow: () => deleteAuthFlow
9970
10431
  });
9971
- import { randomUUID as randomUUID10 } from "crypto";
10432
+ import { randomUUID as randomUUID11 } from "crypto";
9972
10433
  function saveAuthFlow(data) {
9973
10434
  const db = getDatabase();
9974
- const id = randomUUID10();
10435
+ const id = randomUUID11();
9975
10436
  db.prepare("INSERT OR REPLACE INTO auth_flows (id, name, domain, recording_id, storage_state_path) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.domain, data.recordingId ?? null, data.storageStatePath ?? null);
9976
10437
  return getAuthFlow(id);
9977
10438
  }
@@ -10141,6 +10602,226 @@ async function clickByVision(page, description, opts) {
10141
10602
  }
10142
10603
  var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
10143
10604
 
10605
+ // src/lib/api-detector.ts
10606
+ var exports_api_detector = {};
10607
+ __export(exports_api_detector, {
10608
+ listDiscoveredAPIs: () => listDiscoveredAPIs,
10609
+ detectAPIs: () => detectAPIs
10610
+ });
10611
+ import { randomUUID as randomUUID12 } from "crypto";
10612
+ function detectAPIs(sessionId) {
10613
+ const db = getDatabase();
10614
+ const requests = db.query(`SELECT method, url, status_code, response_headers, body_size
10615
+ FROM network_log
10616
+ WHERE session_id = ?
10617
+ AND (response_headers LIKE '%application/json%' OR response_headers LIKE '%text/json%')
10618
+ AND status_code >= 200 AND status_code < 400
10619
+ ORDER BY timestamp DESC`).all(sessionId);
10620
+ const seen = new Map;
10621
+ for (const req of requests) {
10622
+ try {
10623
+ const urlObj = new URL(req.url);
10624
+ if (urlObj.pathname.match(/\.(js|css|png|jpg|svg|woff|ico)$/))
10625
+ continue;
10626
+ if (urlObj.hostname.includes("googleapis.com/identitytoolkit"))
10627
+ continue;
10628
+ if (urlObj.hostname.includes("posthog"))
10629
+ continue;
10630
+ if (urlObj.hostname.includes("sentry"))
10631
+ continue;
10632
+ const key = `${req.method} ${urlObj.origin}${urlObj.pathname}`;
10633
+ if (!seen.has(key)) {
10634
+ seen.set(key, {
10635
+ url: `${urlObj.origin}${urlObj.pathname}`,
10636
+ method: req.method,
10637
+ status_code: req.status_code,
10638
+ content_type: "application/json",
10639
+ response_schema: {},
10640
+ sample_size: req.body_size ?? 0
10641
+ });
10642
+ }
10643
+ } catch {}
10644
+ }
10645
+ const apis = Array.from(seen.values());
10646
+ for (const api of apis) {
10647
+ const id = randomUUID12();
10648
+ db.prepare("INSERT OR IGNORE INTO api_endpoints (id, session_id, url, method, status_code, content_type) VALUES (?, ?, ?, ?, ?, ?)").run(id, sessionId, api.url, api.method, api.status_code, api.content_type);
10649
+ }
10650
+ return apis;
10651
+ }
10652
+ function listDiscoveredAPIs(sessionId) {
10653
+ const db = getDatabase();
10654
+ if (sessionId) {
10655
+ return db.query("SELECT * FROM api_endpoints WHERE session_id = ? ORDER BY discovered_at DESC").all(sessionId);
10656
+ }
10657
+ return db.query("SELECT * FROM api_endpoints ORDER BY discovered_at DESC LIMIT 100").all();
10658
+ }
10659
+ var init_api_detector = __esm(() => {
10660
+ init_schema();
10661
+ });
10662
+
10663
+ // src/lib/structured-extract.ts
10664
+ var exports_structured_extract = {};
10665
+ __export(exports_structured_extract, {
10666
+ extractStructuredData: () => extractStructuredData
10667
+ });
10668
+ async function extractStructuredData(page) {
10669
+ return page.evaluate(() => {
10670
+ const result = { tables: [], lists: [], jsonLd: [], openGraph: {}, metaTags: {}, repeatedElements: [] };
10671
+ document.querySelectorAll("table").forEach((table, idx) => {
10672
+ const headers = [];
10673
+ table.querySelectorAll("thead th, thead td, tr:first-child th").forEach((th) => {
10674
+ headers.push(th.textContent?.trim() ?? "");
10675
+ });
10676
+ const rows = [];
10677
+ table.querySelectorAll("tbody tr, tr:not(:first-child)").forEach((tr) => {
10678
+ const row = [];
10679
+ tr.querySelectorAll("td, th").forEach((td) => {
10680
+ row.push(td.textContent?.trim() ?? "");
10681
+ });
10682
+ if (row.length > 0 && row.some((c) => c !== ""))
10683
+ rows.push(row);
10684
+ });
10685
+ if (rows.length > 0) {
10686
+ result.tables.push({ headers, rows, selector: `table:nth-of-type(${idx + 1})` });
10687
+ }
10688
+ });
10689
+ document.querySelectorAll("ul, ol").forEach((list, idx) => {
10690
+ const items = [];
10691
+ list.querySelectorAll(":scope > li").forEach((li) => {
10692
+ const text = li.textContent?.trim() ?? "";
10693
+ if (text)
10694
+ items.push(text);
10695
+ });
10696
+ if (items.length >= 3) {
10697
+ const tag = list.tagName.toLowerCase();
10698
+ result.lists.push({ items, selector: `${tag}:nth-of-type(${idx + 1})` });
10699
+ }
10700
+ });
10701
+ document.querySelectorAll('script[type="application/ld+json"]').forEach((script) => {
10702
+ try {
10703
+ result.jsonLd.push(JSON.parse(script.textContent ?? ""));
10704
+ } catch {}
10705
+ });
10706
+ document.querySelectorAll('meta[property^="og:"]').forEach((meta) => {
10707
+ const prop = meta.getAttribute("property")?.replace("og:", "") ?? "";
10708
+ result.openGraph[prop] = meta.getAttribute("content") ?? "";
10709
+ });
10710
+ document.querySelectorAll("meta[name]").forEach((meta) => {
10711
+ const name = meta.getAttribute("name") ?? "";
10712
+ if (name)
10713
+ result.metaTags[name] = meta.getAttribute("content") ?? "";
10714
+ });
10715
+ const classCounts = new Map;
10716
+ document.querySelectorAll("[class]").forEach((el) => {
10717
+ const cls = el.className.toString().trim();
10718
+ if (cls && cls.length > 5 && cls.length < 100) {
10719
+ if (!classCounts.has(cls))
10720
+ classCounts.set(cls, []);
10721
+ classCounts.get(cls).push(el);
10722
+ }
10723
+ });
10724
+ for (const [cls, elements] of classCounts) {
10725
+ if (elements.length >= 3 && elements.length <= 200) {
10726
+ const sample = elements.slice(0, 3).map((el) => el.textContent?.trim().slice(0, 100) ?? "");
10727
+ if (sample.some((s) => s.length > 10)) {
10728
+ result.repeatedElements.push({
10729
+ selector: `.${cls.split(" ")[0]}`,
10730
+ count: elements.length,
10731
+ sample
10732
+ });
10733
+ }
10734
+ }
10735
+ }
10736
+ result.repeatedElements.sort((a, b) => b.count - a.count);
10737
+ result.repeatedElements = result.repeatedElements.slice(0, 10);
10738
+ return result;
10739
+ });
10740
+ }
10741
+
10742
+ // src/lib/datasets.ts
10743
+ var exports_datasets = {};
10744
+ __export(exports_datasets, {
10745
+ saveDataset: () => saveDataset,
10746
+ listDatasets: () => listDatasets,
10747
+ getDatasetByName: () => getDatasetByName,
10748
+ getDataset: () => getDataset,
10749
+ exportDataset: () => exportDataset,
10750
+ deleteDataset: () => deleteDataset
10751
+ });
10752
+ import { randomUUID as randomUUID13 } from "crypto";
10753
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync9 } from "fs";
10754
+ import { join as join9 } from "path";
10755
+ import { homedir as homedir9 } from "os";
10756
+ function saveDataset(data) {
10757
+ const db = getDatabase();
10758
+ const id = randomUUID13();
10759
+ const existing = db.query("SELECT id FROM datasets WHERE name = ?").get(data.name);
10760
+ if (existing) {
10761
+ db.prepare("UPDATE datasets SET data = ?, row_count = ?, source_url = ?, schema = ?, last_refresh = datetime('now'), updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(data.rows), data.rows.length, data.sourceUrl ?? null, data.schema ? JSON.stringify(data.schema) : null, data.name);
10762
+ return getDataset(existing.id);
10763
+ }
10764
+ db.prepare("INSERT INTO datasets (id, name, source_url, source_type, data, row_count, schema) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, data.name, data.sourceUrl ?? null, data.sourceType ?? "page", JSON.stringify(data.rows), data.rows.length, data.schema ? JSON.stringify(data.schema) : null);
10765
+ return getDataset(id);
10766
+ }
10767
+ function getDataset(id) {
10768
+ const db = getDatabase();
10769
+ const row = db.query("SELECT * FROM datasets WHERE id = ?").get(id);
10770
+ if (!row)
10771
+ return null;
10772
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
10773
+ }
10774
+ function getDatasetByName(name) {
10775
+ const db = getDatabase();
10776
+ const row = db.query("SELECT * FROM datasets WHERE name = ?").get(name);
10777
+ if (!row)
10778
+ return null;
10779
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
10780
+ }
10781
+ function listDatasets() {
10782
+ const db = getDatabase();
10783
+ return db.query("SELECT id, name, source_url, source_type, row_count, last_refresh, created_at, updated_at FROM datasets ORDER BY updated_at DESC").all().map((row) => ({ ...row, data: `${row.row_count} rows`, schema: null }));
10784
+ }
10785
+ function deleteDataset(name) {
10786
+ const db = getDatabase();
10787
+ return db.prepare("DELETE FROM datasets WHERE name = ?").run(name).changes > 0;
10788
+ }
10789
+ function exportDataset(name, format) {
10790
+ const dataset = getDatasetByName(name);
10791
+ if (!dataset)
10792
+ throw new Error(`Dataset '${name}' not found`);
10793
+ const dir = join9(process.env["BROWSER_DATA_DIR"] ?? join9(homedir9(), ".browser"), "exports");
10794
+ mkdirSync9(dir, { recursive: true });
10795
+ const filename = `${name}.${format}`;
10796
+ const path = join9(dir, filename);
10797
+ if (format === "csv") {
10798
+ const rows = dataset.data;
10799
+ if (rows.length === 0) {
10800
+ writeFileSync3(path, "");
10801
+ return { path, size: 0 };
10802
+ }
10803
+ const headers = Object.keys(rows[0]);
10804
+ const csvLines = [headers.join(",")];
10805
+ for (const row of rows) {
10806
+ csvLines.push(headers.map((h) => {
10807
+ const val = String(row[h] ?? "");
10808
+ return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
10809
+ }).join(","));
10810
+ }
10811
+ const content = csvLines.join(`
10812
+ `);
10813
+ writeFileSync3(path, content);
10814
+ return { path, size: content.length };
10815
+ } else {
10816
+ const content = JSON.stringify(dataset.data, null, 2);
10817
+ writeFileSync3(path, content);
10818
+ return { path, size: content.length };
10819
+ }
10820
+ }
10821
+ var init_datasets = __esm(() => {
10822
+ init_schema();
10823
+ });
10824
+
10144
10825
  // src/lib/auth.ts
10145
10826
  var exports_auth = {};
10146
10827
  __export(exports_auth, {
@@ -10148,18 +10829,18 @@ __export(exports_auth, {
10148
10829
  getCredentials: () => getCredentials
10149
10830
  });
10150
10831
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
10151
- import { join as join9 } from "path";
10152
- import { homedir as homedir9 } from "os";
10832
+ import { join as join10 } from "path";
10833
+ import { homedir as homedir10 } from "os";
10153
10834
  async function getCredentials(service) {
10154
10835
  try {
10155
- const { getSecret } = await import(`${homedir9()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
10836
+ const { getSecret } = await import(`${homedir10()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
10156
10837
  const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
10157
10838
  const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
10158
10839
  if (email?.value && password?.value) {
10159
10840
  return { email: email.value, password: password.value };
10160
10841
  }
10161
10842
  } catch {}
10162
- const secretsPath = join9(homedir9(), ".secrets");
10843
+ const secretsPath = join10(homedir10(), ".secrets");
10163
10844
  if (existsSync5(secretsPath)) {
10164
10845
  const content = readFileSync3(secretsPath, "utf8");
10165
10846
  const lines = content.split(`
@@ -10341,10 +11022,10 @@ __export(exports_dist, {
10341
11022
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
10342
11023
  });
10343
11024
  import { Database as Database2 } from "bun:sqlite";
10344
- import { existsSync as existsSync6, mkdirSync as mkdirSync9 } from "fs";
10345
- import { dirname, join as join10, resolve } from "path";
10346
- import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
10347
- import { homedir as homedir10 } from "os";
11025
+ import { existsSync as existsSync6, mkdirSync as mkdirSync10 } from "fs";
11026
+ import { dirname, join as join11, resolve } from "path";
11027
+ import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
11028
+ import { homedir as homedir11 } from "os";
10348
11029
  import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
10349
11030
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
10350
11031
  import { homedir as homedir22 } from "os";
@@ -10358,7 +11039,7 @@ function isInMemoryDb(path) {
10358
11039
  function findNearestMementosDb(startDir) {
10359
11040
  let dir = resolve(startDir);
10360
11041
  while (true) {
10361
- const candidate = join10(dir, ".mementos", "mementos.db");
11042
+ const candidate = join11(dir, ".mementos", "mementos.db");
10362
11043
  if (existsSync6(candidate))
10363
11044
  return candidate;
10364
11045
  const parent = dirname(dir);
@@ -10371,7 +11052,7 @@ function findNearestMementosDb(startDir) {
10371
11052
  function findGitRoot(startDir) {
10372
11053
  let dir = resolve(startDir);
10373
11054
  while (true) {
10374
- if (existsSync6(join10(dir, ".git")))
11055
+ if (existsSync6(join11(dir, ".git")))
10375
11056
  return dir;
10376
11057
  const parent = dirname(dir);
10377
11058
  if (parent === dir)
@@ -10391,18 +11072,18 @@ function getDbPath() {
10391
11072
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
10392
11073
  const gitRoot = findGitRoot(cwd);
10393
11074
  if (gitRoot) {
10394
- return join10(gitRoot, ".mementos", "mementos.db");
11075
+ return join11(gitRoot, ".mementos", "mementos.db");
10395
11076
  }
10396
11077
  }
10397
11078
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
10398
- return join10(home, ".mementos", "mementos.db");
11079
+ return join11(home, ".mementos", "mementos.db");
10399
11080
  }
10400
11081
  function ensureDir2(filePath) {
10401
11082
  if (isInMemoryDb(filePath))
10402
11083
  return;
10403
11084
  const dir = dirname(resolve(filePath));
10404
11085
  if (!existsSync6(dir)) {
10405
- mkdirSync9(dir, { recursive: true });
11086
+ mkdirSync10(dir, { recursive: true });
10406
11087
  }
10407
11088
  }
10408
11089
  function getDatabase2(dbPath) {
@@ -12252,7 +12933,7 @@ function isValidCategory(value) {
12252
12933
  return VALID_CATEGORIES.includes(value);
12253
12934
  }
12254
12935
  function loadConfig() {
12255
- const configPath = join22(homedir10(), ".mementos", "config.json");
12936
+ const configPath = join22(homedir11(), ".mementos", "config.json");
12256
12937
  let fileConfig = {};
12257
12938
  if (existsSync22(configPath)) {
12258
12939
  try {
@@ -12279,10 +12960,10 @@ function loadConfig() {
12279
12960
  return merged;
12280
12961
  }
12281
12962
  function profilesDir() {
12282
- return join22(homedir10(), ".mementos", "profiles");
12963
+ return join22(homedir11(), ".mementos", "profiles");
12283
12964
  }
12284
12965
  function globalConfigPath() {
12285
- return join22(homedir10(), ".mementos", "config.json");
12966
+ return join22(homedir11(), ".mementos", "config.json");
12286
12967
  }
12287
12968
  function readGlobalConfig() {
12288
12969
  const p = globalConfigPath();
@@ -12297,7 +12978,7 @@ function readGlobalConfig() {
12297
12978
  function writeGlobalConfig(data) {
12298
12979
  const p = globalConfigPath();
12299
12980
  ensureDir22(dirname2(p));
12300
- writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
12981
+ writeFileSync4(p, JSON.stringify(data, null, 2), "utf-8");
12301
12982
  }
12302
12983
  function getActiveProfile() {
12303
12984
  const envProfile = process.env["MEMENTOS_PROFILE"];
@@ -14962,10 +15643,10 @@ __export(exports_dist2, {
14962
15643
  acquireLock: () => acquireLock2
14963
15644
  });
14964
15645
  import { Database as Database3 } from "bun:sqlite";
14965
- import { mkdirSync as mkdirSync10 } from "fs";
14966
- import { join as join11, dirname as dirname3 } from "path";
14967
- import { homedir as homedir11 } from "os";
14968
- import { randomUUID as randomUUID11 } from "crypto";
15646
+ import { mkdirSync as mkdirSync11 } from "fs";
15647
+ import { join as join12, dirname as dirname3 } from "path";
15648
+ import { homedir as homedir12 } from "os";
15649
+ import { randomUUID as randomUUID14 } from "crypto";
14969
15650
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
14970
15651
  import { join as join33 } from "path";
14971
15652
  import { homedir as homedir33 } from "os";
@@ -14973,19 +15654,19 @@ import { readFileSync as readFileSync5 } from "fs";
14973
15654
  import { join as join23 } from "path";
14974
15655
  import { homedir as homedir23 } from "os";
14975
15656
  import { randomUUID as randomUUID22 } from "crypto";
14976
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync4, mkdirSync as mkdirSync33 } from "fs";
15657
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync5, mkdirSync as mkdirSync33 } from "fs";
14977
15658
  import { join as join43, dirname as dirname22 } from "path";
14978
15659
  import { homedir as homedir42 } from "os";
14979
15660
  function getDbPath2() {
14980
15661
  if (process.env.CONVERSATIONS_DB_PATH)
14981
15662
  return process.env.CONVERSATIONS_DB_PATH;
14982
- return join11(homedir11(), ".conversations", "messages.db");
15663
+ return join12(homedir12(), ".conversations", "messages.db");
14983
15664
  }
14984
15665
  function getDb() {
14985
15666
  if (db)
14986
15667
  return db;
14987
15668
  const dbPath = getDbPath2();
14988
- mkdirSync10(dirname3(dbPath), { recursive: true });
15669
+ mkdirSync11(dirname3(dbPath), { recursive: true });
14989
15670
  db = new Database3(dbPath, { create: true });
14990
15671
  db.exec("PRAGMA journal_mode = WAL");
14991
15672
  db.exec("PRAGMA busy_timeout = 5000");
@@ -15345,7 +16026,7 @@ function guessMimeType(name) {
15345
16026
  function sendMessage(opts) {
15346
16027
  const db2 = getDb();
15347
16028
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
15348
- const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID11().slice(0, 8)}`);
16029
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID14().slice(0, 8)}`);
15349
16030
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
15350
16031
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
15351
16032
  const blocking = opts.blocking ? 1 : 0;
@@ -16173,7 +16854,7 @@ function getAutoName() {
16173
16854
  cachedAutoName = name;
16174
16855
  try {
16175
16856
  mkdirSync33(dirname22(AGENT_ID_FILE), { recursive: true });
16176
- writeFileSync4(AGENT_ID_FILE, name + `
16857
+ writeFileSync5(AGENT_ID_FILE, name + `
16177
16858
  `, "utf-8");
16178
16859
  } catch {}
16179
16860
  return name;
@@ -18128,7 +18809,7 @@ Your code should look like:
18128
18809
  }
18129
18810
  }
18130
18811
  }
18131
- function checkPropTypes(typeSpecs, values, location, componentName, element) {
18812
+ function checkPropTypes(typeSpecs, values, location2, componentName, element) {
18132
18813
  {
18133
18814
  var has = Function.call.bind(hasOwnProperty);
18134
18815
  for (var typeSpecName in typeSpecs) {
@@ -18136,23 +18817,23 @@ Your code should look like:
18136
18817
  var error$1 = undefined;
18137
18818
  try {
18138
18819
  if (typeof typeSpecs[typeSpecName] !== "function") {
18139
- var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; " + "it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`." + "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
18820
+ var err = Error((componentName || "React class") + ": " + location2 + " type `" + typeSpecName + "` is invalid; " + "it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`." + "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
18140
18821
  err.name = "Invariant Violation";
18141
18822
  throw err;
18142
18823
  }
18143
- error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
18824
+ error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location2, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
18144
18825
  } catch (ex) {
18145
18826
  error$1 = ex;
18146
18827
  }
18147
18828
  if (error$1 && !(error$1 instanceof Error)) {
18148
18829
  setCurrentlyValidatingElement(element);
18149
- error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
18830
+ error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class", location2, typeSpecName, typeof error$1);
18150
18831
  setCurrentlyValidatingElement(null);
18151
18832
  }
18152
18833
  if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
18153
18834
  loggedTypeFailures[error$1.message] = true;
18154
18835
  setCurrentlyValidatingElement(element);
18155
- error("Failed %s type: %s", location, error$1.message);
18836
+ error("Failed %s type: %s", location2, error$1.message);
18156
18837
  setCurrentlyValidatingElement(null);
18157
18838
  }
18158
18839
  }
@@ -19387,11 +20068,11 @@ __export(exports_dist3, {
19387
20068
  AgentNotFoundError: () => AgentNotFoundError2
19388
20069
  });
19389
20070
  import { Database as Database4 } from "bun:sqlite";
19390
- import { existsSync as existsSync7, mkdirSync as mkdirSync11 } from "fs";
19391
- import { dirname as dirname5, join as join12, resolve as resolve3 } from "path";
20071
+ import { existsSync as existsSync7, mkdirSync as mkdirSync12 } from "fs";
20072
+ import { dirname as dirname5, join as join13, resolve as resolve3 } from "path";
19392
20073
  import { existsSync as existsSync33 } from "fs";
19393
20074
  import { join as join34 } from "path";
19394
- import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
20075
+ import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
19395
20076
  import { join as join24 } from "path";
19396
20077
  import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
19397
20078
  import { join as join44 } from "path";
@@ -19621,7 +20302,7 @@ function isInMemoryDb2(path) {
19621
20302
  function findNearestTodosDb(startDir) {
19622
20303
  let dir = resolve3(startDir);
19623
20304
  while (true) {
19624
- const candidate = join12(dir, ".todos", "todos.db");
20305
+ const candidate = join13(dir, ".todos", "todos.db");
19625
20306
  if (existsSync7(candidate))
19626
20307
  return candidate;
19627
20308
  const parent = dirname5(dir);
@@ -19634,7 +20315,7 @@ function findNearestTodosDb(startDir) {
19634
20315
  function findGitRoot2(startDir) {
19635
20316
  let dir = resolve3(startDir);
19636
20317
  while (true) {
19637
- if (existsSync7(join12(dir, ".git")))
20318
+ if (existsSync7(join13(dir, ".git")))
19638
20319
  return dir;
19639
20320
  const parent = dirname5(dir);
19640
20321
  if (parent === dir)
@@ -19654,18 +20335,18 @@ function getDbPath3() {
19654
20335
  if (process.env["TODOS_DB_SCOPE"] === "project") {
19655
20336
  const gitRoot = findGitRoot2(cwd);
19656
20337
  if (gitRoot) {
19657
- return join12(gitRoot, ".todos", "todos.db");
20338
+ return join13(gitRoot, ".todos", "todos.db");
19658
20339
  }
19659
20340
  }
19660
20341
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
19661
- return join12(home, ".todos", "todos.db");
20342
+ return join13(home, ".todos", "todos.db");
19662
20343
  }
19663
20344
  function ensureDir3(filePath) {
19664
20345
  if (isInMemoryDb2(filePath))
19665
20346
  return;
19666
20347
  const dir = dirname5(resolve3(filePath));
19667
20348
  if (!existsSync7(dir)) {
19668
- mkdirSync11(dir, { recursive: true });
20349
+ mkdirSync12(dir, { recursive: true });
19669
20350
  }
19670
20351
  }
19671
20352
  function getDatabase3(dbPath) {
@@ -20141,7 +20822,7 @@ function readJsonFile(path) {
20141
20822
  }
20142
20823
  }
20143
20824
  function writeJsonFile(path, data) {
20144
- writeFileSync5(path, JSON.stringify(data, null, 2) + `
20825
+ writeFileSync6(path, JSON.stringify(data, null, 2) + `
20145
20826
  `);
20146
20827
  }
20147
20828
  function readHighWaterMark(dir) {
@@ -20152,7 +20833,7 @@ function readHighWaterMark(dir) {
20152
20833
  return isNaN(val) ? 1 : val;
20153
20834
  }
20154
20835
  function writeHighWaterMark(dir, value) {
20155
- writeFileSync5(join24(dir, ".highwatermark"), String(value));
20836
+ writeFileSync6(join24(dir, ".highwatermark"), String(value));
20156
20837
  }
20157
20838
  function getFileMtimeMs(path) {
20158
20839
  try {
@@ -24742,9 +25423,9 @@ __export(exports_dist4, {
24742
25423
  CATEGORIES: () => CATEGORIES,
24743
25424
  AGENT_TARGETS: () => AGENT_TARGETS
24744
25425
  });
24745
- import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
24746
- import { join as join13, dirname as dirname6 } from "path";
24747
- import { homedir as homedir12 } from "os";
25426
+ import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync13, writeFileSync as writeFileSync7, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
25427
+ import { join as join14, dirname as dirname6 } from "path";
25428
+ import { homedir as homedir13 } from "os";
24748
25429
  import { fileURLToPath } from "url";
24749
25430
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
24750
25431
  import { join as join25 } from "path";
@@ -24855,17 +25536,17 @@ function normalizeSkillName(name) {
24855
25536
  function findSkillsDir() {
24856
25537
  let dir = __dirname2;
24857
25538
  for (let i = 0;i < 5; i++) {
24858
- const candidate = join13(dir, "skills");
25539
+ const candidate = join14(dir, "skills");
24859
25540
  if (existsSync8(candidate)) {
24860
25541
  return candidate;
24861
25542
  }
24862
25543
  dir = dirname6(dir);
24863
25544
  }
24864
- return join13(__dirname2, "..", "skills");
25545
+ return join14(__dirname2, "..", "skills");
24865
25546
  }
24866
25547
  function getSkillPath(name) {
24867
25548
  const skillName = normalizeSkillName(name);
24868
- return join13(SKILLS_DIR, skillName);
25549
+ return join14(SKILLS_DIR, skillName);
24869
25550
  }
24870
25551
  function skillExists(name) {
24871
25552
  return existsSync8(getSkillPath(name));
@@ -24874,8 +25555,8 @@ function installSkill(name, options = {}) {
24874
25555
  const { targetDir = process.cwd(), overwrite = false } = options;
24875
25556
  const skillName = normalizeSkillName(name);
24876
25557
  const sourcePath = getSkillPath(name);
24877
- const destDir = join13(targetDir, ".skills");
24878
- const destPath = join13(destDir, skillName);
25558
+ const destDir = join14(targetDir, ".skills");
25559
+ const destPath = join14(destDir, skillName);
24879
25560
  if (!existsSync8(sourcePath)) {
24880
25561
  return {
24881
25562
  skill: name,
@@ -24893,7 +25574,7 @@ function installSkill(name, options = {}) {
24893
25574
  }
24894
25575
  try {
24895
25576
  if (!existsSync8(destDir)) {
24896
- mkdirSync12(destDir, { recursive: true });
25577
+ mkdirSync13(destDir, { recursive: true });
24897
25578
  }
24898
25579
  if (existsSync8(destPath) && overwrite) {
24899
25580
  rmSync2(destPath, { recursive: true, force: true });
@@ -24934,7 +25615,7 @@ function installSkills(names, options = {}) {
24934
25615
  return names.map((name) => installSkill(name, options));
24935
25616
  }
24936
25617
  function updateSkillsIndex(skillsDir) {
24937
- const indexPath = join13(skillsDir, "index.ts");
25618
+ const indexPath = join14(skillsDir, "index.ts");
24938
25619
  const meta = loadMeta(skillsDir);
24939
25620
  const disabledSet = new Set(meta.disabled || []);
24940
25621
  const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
@@ -24950,10 +25631,10 @@ function updateSkillsIndex(skillsDir) {
24950
25631
 
24951
25632
  ${exports}
24952
25633
  `;
24953
- writeFileSync6(indexPath, content);
25634
+ writeFileSync7(indexPath, content);
24954
25635
  }
24955
25636
  function getMetaPath(skillsDir) {
24956
- return join13(skillsDir, ".meta.json");
25637
+ return join14(skillsDir, ".meta.json");
24957
25638
  }
24958
25639
  function loadMeta(skillsDir) {
24959
25640
  const metaPath2 = getMetaPath(skillsDir);
@@ -24965,14 +25646,14 @@ function loadMeta(skillsDir) {
24965
25646
  return { skills: {} };
24966
25647
  }
24967
25648
  function saveMeta(skillsDir, meta) {
24968
- writeFileSync6(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
25649
+ writeFileSync7(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
24969
25650
  }
24970
25651
  function recordInstall(skillsDir, name) {
24971
25652
  const meta = loadMeta(skillsDir);
24972
25653
  const skillName = normalizeSkillName(name);
24973
25654
  let version = "unknown";
24974
25655
  try {
24975
- const pkgPath = join13(skillsDir, skillName, "package.json");
25656
+ const pkgPath = join14(skillsDir, skillName, "package.json");
24976
25657
  if (existsSync8(pkgPath)) {
24977
25658
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
24978
25659
  version = pkg.version || "unknown";
@@ -24987,12 +25668,12 @@ function recordRemove(skillsDir, name) {
24987
25668
  saveMeta(skillsDir, meta);
24988
25669
  }
24989
25670
  function getInstallMeta(targetDir = process.cwd()) {
24990
- return loadMeta(join13(targetDir, ".skills"));
25671
+ return loadMeta(join14(targetDir, ".skills"));
24991
25672
  }
24992
25673
  function disableSkill(name, targetDir = process.cwd()) {
24993
- const skillsDir = join13(targetDir, ".skills");
25674
+ const skillsDir = join14(targetDir, ".skills");
24994
25675
  const skillName = normalizeSkillName(name);
24995
- if (!existsSync8(join13(skillsDir, skillName)))
25676
+ if (!existsSync8(join14(skillsDir, skillName)))
24996
25677
  return false;
24997
25678
  const meta = loadMeta(skillsDir);
24998
25679
  const disabled = new Set(meta.disabled || []);
@@ -25005,7 +25686,7 @@ function disableSkill(name, targetDir = process.cwd()) {
25005
25686
  return true;
25006
25687
  }
25007
25688
  function enableSkill(name, targetDir = process.cwd()) {
25008
- const skillsDir = join13(targetDir, ".skills");
25689
+ const skillsDir = join14(targetDir, ".skills");
25009
25690
  const meta = loadMeta(skillsDir);
25010
25691
  const disabled = new Set(meta.disabled || []);
25011
25692
  if (!disabled.has(name))
@@ -25017,23 +25698,23 @@ function enableSkill(name, targetDir = process.cwd()) {
25017
25698
  return true;
25018
25699
  }
25019
25700
  function getDisabledSkills(targetDir = process.cwd()) {
25020
- const meta = loadMeta(join13(targetDir, ".skills"));
25701
+ const meta = loadMeta(join14(targetDir, ".skills"));
25021
25702
  return meta.disabled || [];
25022
25703
  }
25023
25704
  function getInstalledSkills(targetDir = process.cwd()) {
25024
- const skillsDir = join13(targetDir, ".skills");
25705
+ const skillsDir = join14(targetDir, ".skills");
25025
25706
  if (!existsSync8(skillsDir)) {
25026
25707
  return [];
25027
25708
  }
25028
25709
  return readdirSync6(skillsDir).filter((f) => {
25029
- const fullPath = join13(skillsDir, f);
25710
+ const fullPath = join14(skillsDir, f);
25030
25711
  return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
25031
25712
  }).map((f) => f.replace("skill-", ""));
25032
25713
  }
25033
25714
  function removeSkill(name, targetDir = process.cwd()) {
25034
25715
  const skillName = normalizeSkillName(name);
25035
- const skillsDir = join13(targetDir, ".skills");
25036
- const skillPath = join13(skillsDir, skillName);
25716
+ const skillsDir = join14(targetDir, ".skills");
25717
+ const skillPath = join14(skillsDir, skillName);
25037
25718
  if (!existsSync8(skillPath)) {
25038
25719
  return false;
25039
25720
  }
@@ -25045,13 +25726,13 @@ function removeSkill(name, targetDir = process.cwd()) {
25045
25726
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
25046
25727
  const agentDir = `.${agent}`;
25047
25728
  if (scope === "project") {
25048
- return join13(projectDir || process.cwd(), agentDir, "skills");
25729
+ return join14(projectDir || process.cwd(), agentDir, "skills");
25049
25730
  }
25050
- return join13(homedir12(), agentDir, "skills");
25731
+ return join14(homedir13(), agentDir, "skills");
25051
25732
  }
25052
25733
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
25053
25734
  const skillName = normalizeSkillName(name);
25054
- return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
25735
+ return join14(getAgentSkillsDir(agent, scope, projectDir), skillName);
25055
25736
  }
25056
25737
  function installSkillForAgent(name, options, generateSkillMd) {
25057
25738
  const { agent, scope = "global", projectDir } = options;
@@ -25061,7 +25742,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
25061
25742
  return { skill: name, success: false, error: `Skill '${name}' not found` };
25062
25743
  }
25063
25744
  let skillMdContent = null;
25064
- const skillMdPath = join13(sourcePath, "SKILL.md");
25745
+ const skillMdPath = join14(sourcePath, "SKILL.md");
25065
25746
  if (existsSync8(skillMdPath)) {
25066
25747
  skillMdContent = readFileSync7(skillMdPath, "utf-8");
25067
25748
  } else if (generateSkillMd) {
@@ -25072,7 +25753,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
25072
25753
  }
25073
25754
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
25074
25755
  if (scope === "global") {
25075
- const agentBaseDir2 = join13(homedir12(), `.${agent}`);
25756
+ const agentBaseDir2 = join14(homedir13(), `.${agent}`);
25076
25757
  if (!existsSync8(agentBaseDir2)) {
25077
25758
  const agentLabels = {
25078
25759
  claude: "Claude Code",
@@ -25096,8 +25777,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
25096
25777
  }
25097
25778
  }
25098
25779
  try {
25099
- mkdirSync12(destDir, { recursive: true });
25100
- writeFileSync6(join13(destDir, "SKILL.md"), skillMdContent);
25780
+ mkdirSync13(destDir, { recursive: true });
25781
+ writeFileSync7(join14(destDir, "SKILL.md"), skillMdContent);
25101
25782
  return { skill: name, success: true, path: destDir };
25102
25783
  } catch (error) {
25103
25784
  return {
@@ -27034,7 +27715,7 @@ __export(exports_cron_manager, {
27034
27715
  deleteCronJob: () => deleteCronJob,
27035
27716
  createCronJob: () => createCronJob
27036
27717
  });
27037
- import { randomUUID as randomUUID12 } from "crypto";
27718
+ import { randomUUID as randomUUID15 } from "crypto";
27038
27719
  function ensureCronTable() {
27039
27720
  const db2 = getDatabase();
27040
27721
  db2.exec(`
@@ -27063,7 +27744,7 @@ function ensureCronTable() {
27063
27744
  function createCronJob(schedule, task, name) {
27064
27745
  ensureCronTable();
27065
27746
  const db2 = getDatabase();
27066
- const id = randomUUID12();
27747
+ const id = randomUUID15();
27067
27748
  db2.prepare(`
27068
27749
  INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
27069
27750
  VALUES (?, ?, ?, ?, 1)
@@ -27121,7 +27802,7 @@ function getCronEvents(jobId, limit = 10) {
27121
27802
  async function executeCronJob(job) {
27122
27803
  ensureCronTable();
27123
27804
  const db2 = getDatabase();
27124
- const eventId = randomUUID12();
27805
+ const eventId = randomUUID15();
27125
27806
  const startedAt = new Date().toISOString();
27126
27807
  db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
27127
27808
  try {
@@ -27212,7 +27893,7 @@ __export(exports_url_watcher, {
27212
27893
  deleteWatchJob: () => deleteWatchJob,
27213
27894
  createWatchJob: () => createWatchJob
27214
27895
  });
27215
- import { randomUUID as randomUUID13 } from "crypto";
27896
+ import { randomUUID as randomUUID16 } from "crypto";
27216
27897
  import { createHash } from "crypto";
27217
27898
  function ensureWatchTables() {
27218
27899
  const db2 = getDatabase();
@@ -27244,7 +27925,7 @@ function ensureWatchTables() {
27244
27925
  function createWatchJob(url, schedule, opts) {
27245
27926
  ensureWatchTables();
27246
27927
  const db2 = getDatabase();
27247
- const id = randomUUID13();
27928
+ const id = randomUUID16();
27248
27929
  db2.prepare(`
27249
27930
  INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
27250
27931
  VALUES (?, ?, ?, ?, ?, ?, 1)
@@ -27280,7 +27961,7 @@ function getWatchEvents(watchId, limit = 20) {
27280
27961
  async function checkWatchJob(job) {
27281
27962
  ensureWatchTables();
27282
27963
  const db2 = getDatabase();
27283
- const eventId = randomUUID13();
27964
+ const eventId = randomUUID16();
27284
27965
  const checkedAt = new Date().toISOString();
27285
27966
  let newContent = "";
27286
27967
  try {
@@ -31432,7 +32113,7 @@ var NEVER = INVALID;
31432
32113
  init_session();
31433
32114
  init_actions();
31434
32115
  import { readFileSync as readFileSync8 } from "fs";
31435
- import { join as join14 } from "path";
32116
+ import { join as join15 } from "path";
31436
32117
 
31437
32118
  // src/lib/screenshot.ts
31438
32119
  init_types();
@@ -32202,7 +32883,7 @@ async function closeTab(page, index) {
32202
32883
  init_dialogs();
32203
32884
  init_profiles();
32204
32885
  init_types();
32205
- var _pkg = JSON.parse(readFileSync8(join14(import.meta.dir, "../../package.json"), "utf8"));
32886
+ var _pkg = JSON.parse(readFileSync8(join15(import.meta.dir, "../../package.json"), "utf8"));
32206
32887
  var networkLogCleanup = new Map;
32207
32888
  var consoleCaptureCleanup = new Map;
32208
32889
  var harCaptures = new Map;
@@ -32320,6 +33001,25 @@ server.tool("browser_session_close", "Close a browser session", { session_id: ex
32320
33001
  return err(e);
32321
33002
  }
32322
33003
  });
33004
+ server.tool("browser_session_fork", "Fork a session: create a new session with the same auth state (cookies, storage) and URL as an existing one. Like git branch for browser sessions.", { source_session_id: exports_external.string(), name: exports_external.string().optional() }, async ({ source_session_id, name }) => {
33005
+ try {
33006
+ const sourcePage = getSessionPage(source_session_id);
33007
+ const sourceUrl = sourcePage.url();
33008
+ const tempName = `_fork_${Date.now()}`;
33009
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33010
+ await saveStateFromPage2(sourcePage, tempName);
33011
+ const { session, page } = await createSession2({
33012
+ storageState: tempName,
33013
+ startUrl: sourceUrl,
33014
+ name: name ?? `fork-of-${source_session_id.slice(0, 8)}`
33015
+ });
33016
+ const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33017
+ deleteState2(tempName);
33018
+ return json({ forked_session: session, source_url: sourceUrl });
33019
+ } catch (e) {
33020
+ return err(e);
33021
+ }
33022
+ });
32323
33023
  server.tool("browser_session_timeline", "Get chronological action log for a session", { session_id: exports_external.string().optional(), limit: exports_external.number().optional().default(50) }, async ({ session_id, limit }) => {
32324
33024
  try {
32325
33025
  const sid = resolveSessionId(session_id);
@@ -32913,6 +33613,52 @@ server.tool("browser_har_stop", "Stop HAR capture and return the HAR data", { se
32913
33613
  return err(e);
32914
33614
  }
32915
33615
  });
33616
+ server.tool("browser_intercept_response", "Intercept and modify API responses for testing. Mock data, simulate errors, add latency.", {
33617
+ session_id: exports_external.string().optional(),
33618
+ url_pattern: exports_external.string().describe("URL pattern to intercept (e.g. '**/api/users*')"),
33619
+ action: exports_external.enum(["mock", "delay", "error"]).describe("What to do with matched requests"),
33620
+ mock_body: exports_external.string().optional().describe("Response body for mock action"),
33621
+ mock_content_type: exports_external.string().optional().default("application/json"),
33622
+ status_code: exports_external.number().optional().default(200).describe("HTTP status code (for mock/error)"),
33623
+ delay_ms: exports_external.number().optional().default(3000).describe("Delay in ms (for delay action)")
33624
+ }, async ({ session_id, url_pattern, action, mock_body, mock_content_type, status_code, delay_ms }) => {
33625
+ try {
33626
+ const sid = resolveSessionId(session_id);
33627
+ const page = getSessionPage(sid);
33628
+ await page.route(url_pattern, async (route) => {
33629
+ if (action === "mock") {
33630
+ await route.fulfill({
33631
+ status: status_code,
33632
+ contentType: mock_content_type,
33633
+ body: mock_body ?? "{}"
33634
+ });
33635
+ } else if (action === "error") {
33636
+ await route.fulfill({
33637
+ status: status_code ?? 500,
33638
+ contentType: "application/json",
33639
+ body: JSON.stringify({ error: "Intercepted error", status: status_code })
33640
+ });
33641
+ } else if (action === "delay") {
33642
+ await new Promise((r) => setTimeout(r, delay_ms));
33643
+ await route.continue();
33644
+ }
33645
+ });
33646
+ logEvent(sid, "intercept_set", { url_pattern, action, status_code });
33647
+ return json({ intercepted: true, url_pattern, action });
33648
+ } catch (e) {
33649
+ return err(e);
33650
+ }
33651
+ });
33652
+ server.tool("browser_intercept_clear", "Remove all response intercepts from a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33653
+ try {
33654
+ const sid = resolveSessionId(session_id);
33655
+ const page = getSessionPage(sid);
33656
+ await page.unrouteAll({ behavior: "ignoreErrors" });
33657
+ return json({ cleared: true });
33658
+ } catch (e) {
33659
+ return err(e);
33660
+ }
33661
+ });
32916
33662
  server.tool("browser_performance", "Get performance metrics for the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32917
33663
  try {
32918
33664
  const sid = resolveSessionId(session_id);
@@ -32923,6 +33669,72 @@ server.tool("browser_performance", "Get performance metrics for the current page
32923
33669
  return err(e);
32924
33670
  }
32925
33671
  });
33672
+ server.tool("browser_detect_env", "Detect if the current page is running in production, development, staging, or local environment. Analyzes URL, meta tags, source maps, analytics SDKs, and more.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33673
+ try {
33674
+ const sid = resolveSessionId(session_id);
33675
+ const page = getSessionPage(sid);
33676
+ const { detectEnvironment: detectEnvironment2 } = await Promise.resolve().then(() => exports_env_detector);
33677
+ const result = await detectEnvironment2(page);
33678
+ return json(result);
33679
+ } catch (e) {
33680
+ return err(e);
33681
+ }
33682
+ });
33683
+ server.tool("browser_performance_deep", "Deep performance analysis: Web Vitals, resource breakdown by type, largest resources, third-party scripts with categories, DOM complexity, memory usage.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33684
+ try {
33685
+ const sid = resolveSessionId(session_id);
33686
+ const page = getSessionPage(sid);
33687
+ const { getDeepPerformance: getDeepPerformance2 } = await Promise.resolve().then(() => (init_deep_performance(), exports_deep_performance));
33688
+ const result = await getDeepPerformance2(page);
33689
+ return json(result);
33690
+ } catch (e) {
33691
+ return err(e);
33692
+ }
33693
+ });
33694
+ server.tool("browser_accessibility_audit", "Run accessibility audit on the page. Injects axe-core and returns violations grouped by severity (critical, serious, moderate, minor).", { session_id: exports_external.string().optional(), selector: exports_external.string().optional().describe("Scope audit to a specific element") }, async ({ session_id, selector }) => {
33695
+ try {
33696
+ const sid = resolveSessionId(session_id);
33697
+ const page = getSessionPage(sid);
33698
+ await page.evaluate(`
33699
+ if (!window.axe) {
33700
+ const script = document.createElement('script');
33701
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js';
33702
+ document.head.appendChild(script);
33703
+ await new Promise((resolve, reject) => {
33704
+ script.onload = resolve;
33705
+ script.onerror = reject;
33706
+ });
33707
+ }
33708
+ `);
33709
+ await new Promise((r) => setTimeout(r, 500));
33710
+ const results = await page.evaluate((sel) => {
33711
+ const opts = {};
33712
+ if (sel)
33713
+ opts.include = [sel];
33714
+ return window.axe.run(opts.include ? { include: [sel] } : document).then((r) => ({
33715
+ violations: r.violations.map((v) => ({
33716
+ id: v.id,
33717
+ impact: v.impact,
33718
+ description: v.description,
33719
+ help: v.help,
33720
+ helpUrl: v.helpUrl,
33721
+ nodes_count: v.nodes.length,
33722
+ selectors: v.nodes.slice(0, 3).map((n) => n.target?.[0] ?? "")
33723
+ })),
33724
+ passes: r.passes.length,
33725
+ violations_count: r.violations.length,
33726
+ incomplete: r.incomplete.length
33727
+ }));
33728
+ }, selector);
33729
+ const byImpact = { critical: 0, serious: 0, moderate: 0, minor: 0 };
33730
+ for (const v of results.violations) {
33731
+ byImpact[v.impact] = (byImpact[v.impact] || 0) + 1;
33732
+ }
33733
+ return json({ ...results, by_impact: byImpact, score: Math.max(0, 100 - results.violations_count * 5) });
33734
+ } catch (e) {
33735
+ return err(e);
33736
+ }
33737
+ });
32926
33738
  server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string().optional(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
32927
33739
  try {
32928
33740
  const sid = resolveSessionId(session_id);
@@ -32986,6 +33798,46 @@ server.tool("browser_recordings_list", "List all recordings", { project_id: expo
32986
33798
  return err(e);
32987
33799
  }
32988
33800
  });
33801
+ server.tool("browser_workflow_save", "Save a recording as a reusable workflow with self-healing replay", { recording_id: exports_external.string(), name: exports_external.string(), description: exports_external.string().optional() }, async ({ recording_id, name, description }) => {
33802
+ try {
33803
+ const { saveWorkflowFromRecording: saveWorkflowFromRecording2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33804
+ return json(saveWorkflowFromRecording2(recording_id, name, description));
33805
+ } catch (e) {
33806
+ return err(e);
33807
+ }
33808
+ });
33809
+ server.tool("browser_workflow_list", "List all saved workflows", {}, async () => {
33810
+ try {
33811
+ const { listWorkflows: listWorkflows2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33812
+ const workflows = listWorkflows2();
33813
+ return json({ workflows: workflows.map((w) => ({ ...w, steps: `${w.steps.length} steps` })), count: workflows.length });
33814
+ } catch (e) {
33815
+ return err(e);
33816
+ }
33817
+ });
33818
+ server.tool("browser_workflow_run", "Run a saved workflow with self-healing. If selectors changed, auto-adapts and reports what was healed.", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
33819
+ try {
33820
+ const sid = resolveSessionId(session_id);
33821
+ const page = getSessionPage(sid);
33822
+ const { getWorkflowByName: getWorkflowByName2, runWorkflow: runWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33823
+ const workflow = getWorkflowByName2(name);
33824
+ if (!workflow)
33825
+ return err(new Error(`Workflow '${name}' not found`));
33826
+ const result = await runWorkflow2(workflow, page);
33827
+ logEvent(sid, "workflow_run", { name, ...result });
33828
+ return json(result);
33829
+ } catch (e) {
33830
+ return err(e);
33831
+ }
33832
+ });
33833
+ server.tool("browser_workflow_delete", "Delete a saved workflow", { name: exports_external.string() }, async ({ name }) => {
33834
+ try {
33835
+ const { deleteWorkflow: deleteWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33836
+ return json({ deleted: deleteWorkflow2(name) });
33837
+ } catch (e) {
33838
+ return err(e);
33839
+ }
33840
+ });
32989
33841
  server.tool("browser_crawl", "Crawl a URL recursively and return discovered pages", {
32990
33842
  url: exports_external.string(),
32991
33843
  max_depth: exports_external.number().optional().default(2),
@@ -33284,6 +34136,61 @@ server.tool("browser_find_visual", "Find an element using AI vision when selecto
33284
34136
  return err(e);
33285
34137
  }
33286
34138
  });
34139
+ server.tool("browser_wait_for_idle", "Wait until no network requests are in-flight for a specified duration. Essential for SPAs that load data after navigation.", {
34140
+ session_id: exports_external.string().optional(),
34141
+ idle_time: exports_external.number().optional().default(2000).describe("How long (ms) network must be idle to consider page loaded"),
34142
+ timeout: exports_external.number().optional().default(30000).describe("Max wait time (ms) before giving up")
34143
+ }, async ({ session_id, idle_time, timeout }) => {
34144
+ try {
34145
+ const sid = resolveSessionId(session_id);
34146
+ const page = getSessionPage(sid);
34147
+ const t0 = Date.now();
34148
+ let lastActivity = Date.now();
34149
+ let pending = 0;
34150
+ const onRequest = () => {
34151
+ pending++;
34152
+ lastActivity = Date.now();
34153
+ };
34154
+ const onResponse = () => {
34155
+ pending = Math.max(0, pending - 1);
34156
+ if (pending === 0)
34157
+ lastActivity = Date.now();
34158
+ };
34159
+ const onFailed = () => {
34160
+ pending = Math.max(0, pending - 1);
34161
+ if (pending === 0)
34162
+ lastActivity = Date.now();
34163
+ };
34164
+ page.on("request", onRequest);
34165
+ page.on("response", onResponse);
34166
+ page.on("requestfailed", onFailed);
34167
+ try {
34168
+ await new Promise((resolve4, reject) => {
34169
+ const check = () => {
34170
+ const now3 = Date.now();
34171
+ if (now3 - t0 > timeout) {
34172
+ reject(new Error(`Timeout after ${timeout}ms (${pending} requests still pending)`));
34173
+ return;
34174
+ }
34175
+ if (pending === 0 && now3 - lastActivity >= idle_time) {
34176
+ resolve4();
34177
+ return;
34178
+ }
34179
+ setTimeout(check, 100);
34180
+ };
34181
+ check();
34182
+ });
34183
+ } finally {
34184
+ page.removeListener("request", onRequest);
34185
+ page.removeListener("response", onResponse);
34186
+ page.removeListener("requestfailed", onFailed);
34187
+ }
34188
+ const waited_ms = Date.now() - t0;
34189
+ return json({ idle: true, waited_ms, pending_requests: 0 });
34190
+ } catch (e) {
34191
+ return err(e);
34192
+ }
34193
+ });
33287
34194
  server.tool("browser_wait_for_text", "Wait until specific text appears on the page", { session_id: exports_external.string().optional(), text: exports_external.string(), timeout: exports_external.number().optional().default(1e4), exact: exports_external.boolean().optional().default(false) }, async ({ session_id, text, timeout, exact }) => {
33288
34195
  try {
33289
34196
  const sid = resolveSessionId(session_id);
@@ -33681,6 +34588,68 @@ server.tool("browser_profile_delete", "Delete a saved browser profile", { name:
33681
34588
  return err(e);
33682
34589
  }
33683
34590
  });
34591
+ server.tool("browser_detect_apis", "Scan network traffic for JSON API endpoints. Returns discovered endpoints with methods, status codes, and URLs.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34592
+ try {
34593
+ const sid = resolveSessionId(session_id);
34594
+ const { detectAPIs: detectAPIs2 } = await Promise.resolve().then(() => (init_api_detector(), exports_api_detector));
34595
+ const apis = detectAPIs2(sid);
34596
+ return json({ apis, count: apis.length });
34597
+ } catch (e) {
34598
+ return err(e);
34599
+ }
34600
+ });
34601
+ server.tool("browser_extract_structured", "Extract structured data from page: tables, lists, JSON-LD, Open Graph, meta tags, and repeated elements (cards/items).", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34602
+ try {
34603
+ const sid = resolveSessionId(session_id);
34604
+ const page = getSessionPage(sid);
34605
+ const { extractStructuredData: extractStructuredData2 } = await Promise.resolve().then(() => exports_structured_extract);
34606
+ const data = await extractStructuredData2(page);
34607
+ return json({
34608
+ tables: data.tables.length,
34609
+ lists: data.lists.length,
34610
+ json_ld: data.jsonLd.length,
34611
+ open_graph: Object.keys(data.openGraph).length,
34612
+ meta_tags: Object.keys(data.metaTags).length,
34613
+ repeated_elements: data.repeatedElements.length,
34614
+ data
34615
+ });
34616
+ } catch (e) {
34617
+ return err(e);
34618
+ }
34619
+ });
34620
+ server.tool("browser_dataset_save", "Save extracted data as a named dataset for later use", { name: exports_external.string(), data: exports_external.array(exports_external.record(exports_external.unknown())), source_url: exports_external.string().optional() }, async ({ name, data, source_url }) => {
34621
+ try {
34622
+ const { saveDataset: saveDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34623
+ const dataset = saveDataset2({ name, rows: data, sourceUrl: source_url });
34624
+ return json({ id: dataset.id, name: dataset.name, row_count: dataset.row_count });
34625
+ } catch (e) {
34626
+ return err(e);
34627
+ }
34628
+ });
34629
+ server.tool("browser_dataset_list", "List all saved datasets", {}, async () => {
34630
+ try {
34631
+ const { listDatasets: listDatasets2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34632
+ return json({ datasets: listDatasets2() });
34633
+ } catch (e) {
34634
+ return err(e);
34635
+ }
34636
+ });
34637
+ server.tool("browser_dataset_export", "Export a dataset as JSON or CSV file", { name: exports_external.string(), format: exports_external.enum(["json", "csv"]).optional().default("json") }, async ({ name, format }) => {
34638
+ try {
34639
+ const { exportDataset: exportDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34640
+ return json(exportDataset2(name, format));
34641
+ } catch (e) {
34642
+ return err(e);
34643
+ }
34644
+ });
34645
+ server.tool("browser_dataset_delete", "Delete a saved dataset", { name: exports_external.string() }, async ({ name }) => {
34646
+ try {
34647
+ const { deleteDataset: deleteDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34648
+ return json({ deleted: deleteDataset2(name) });
34649
+ } catch (e) {
34650
+ return err(e);
34651
+ }
34652
+ });
33684
34653
  server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
33685
34654
  try {
33686
34655
  const groups = {
@@ -33689,7 +34658,8 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
33689
34658
  { tool: "browser_back", description: "Navigate back in history" },
33690
34659
  { tool: "browser_forward", description: "Navigate forward in history" },
33691
34660
  { tool: "browser_reload", description: "Reload the current page" },
33692
- { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" }
34661
+ { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" },
34662
+ { tool: "browser_wait_for_idle", description: "Wait for network idle (no pending requests)" }
33693
34663
  ],
33694
34664
  Interaction: [
33695
34665
  { tool: "browser_click", description: "Click element by ref or selector" },
@@ -33742,7 +34712,9 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
33742
34712
  { tool: "browser_network_log", description: "Get captured network requests" },
33743
34713
  { tool: "browser_network_intercept", description: "Add a network interception rule" },
33744
34714
  { tool: "browser_har_start", description: "Start HAR capture" },
33745
- { tool: "browser_har_stop", description: "Stop HAR capture and get data" }
34715
+ { tool: "browser_har_stop", description: "Stop HAR capture and get data" },
34716
+ { tool: "browser_intercept_response", description: "Mock/delay/error API responses for testing" },
34717
+ { tool: "browser_intercept_clear", description: "Remove all response intercepts" }
33746
34718
  ],
33747
34719
  Performance: [
33748
34720
  { tool: "browser_performance", description: "Get performance metrics" }
@@ -33767,6 +34739,20 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
33767
34739
  { tool: "browser_auth_list", description: "List all saved auth flows" },
33768
34740
  { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
33769
34741
  ],
34742
+ Workflows: [
34743
+ { tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
34744
+ { tool: "browser_workflow_list", description: "List all saved workflows" },
34745
+ { tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
34746
+ { tool: "browser_workflow_delete", description: "Delete a saved workflow" }
34747
+ ],
34748
+ Data: [
34749
+ { tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
34750
+ { tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
34751
+ { tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
34752
+ { tool: "browser_dataset_list", description: "List all saved datasets" },
34753
+ { tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
34754
+ { tool: "browser_dataset_delete", description: "Delete a saved dataset" }
34755
+ ],
33770
34756
  Crawl: [
33771
34757
  { tool: "browser_crawl", description: "Crawl a URL recursively" }
33772
34758
  ],
@@ -33811,6 +34797,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
33811
34797
  { tool: "browser_session_untag", description: "Remove a tag from a session" },
33812
34798
  { tool: "browser_session_stats", description: "Get session stats and token usage" },
33813
34799
  { tool: "browser_session_timeline", description: "Get chronological action log" },
34800
+ { tool: "browser_session_fork", description: "Fork a session (same auth state + URL)" },
33814
34801
  { tool: "browser_tab_new", description: "Open a new tab" },
33815
34802
  { tool: "browser_tab_list", description: "List all open tabs" },
33816
34803
  { tool: "browser_tab_switch", description: "Switch to a tab by index" },
@@ -33820,6 +34807,9 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
33820
34807
  { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
33821
34808
  { tool: "browser_version", description: "Show running binary version and tool count" },
33822
34809
  { tool: "browser_help", description: "Show this help (all tools)" },
34810
+ { tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
34811
+ { tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
34812
+ { tool: "browser_accessibility_audit", description: "Run axe-core accessibility audit with severity breakdown" },
33823
34813
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
33824
34814
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
33825
34815
  { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },