lobster-cli 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/lib.js CHANGED
@@ -432,7 +432,7 @@ function extractJsonFromString(str) {
432
432
 
433
433
  // src/browser/manager.ts
434
434
  import puppeteer from "puppeteer-core";
435
- import { existsSync } from "fs";
435
+ import { existsSync as existsSync3 } from "fs";
436
436
 
437
437
  // src/utils/logger.ts
438
438
  import chalk from "chalk";
@@ -448,20 +448,591 @@ var log = {
448
448
  dim: (msg) => console.log(chalk.dim(msg))
449
449
  };
450
450
 
451
+ // src/browser/profiles.ts
452
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync, statSync } from "fs";
453
+ import { join as join2 } from "path";
454
+
455
+ // src/config/index.ts
456
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
457
+ import { join } from "path";
458
+ import { homedir } from "os";
459
+ import yaml from "js-yaml";
460
+
461
+ // src/config/schema.ts
462
+ import { z } from "zod";
463
+ var LLM_PROVIDERS = {
464
+ openai: {
465
+ name: "OpenAI",
466
+ baseURL: "https://api.openai.com/v1",
467
+ defaultModel: "gpt-4o",
468
+ keyPrefix: "sk-",
469
+ keyEnvHint: "https://platform.openai.com/api-keys",
470
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"]
471
+ },
472
+ anthropic: {
473
+ name: "Anthropic",
474
+ baseURL: "https://api.anthropic.com/v1",
475
+ defaultModel: "claude-sonnet-4-20250514",
476
+ keyPrefix: "sk-ant-",
477
+ keyEnvHint: "https://console.anthropic.com/settings/keys",
478
+ models: ["claude-opus-4-20250514", "claude-sonnet-4-20250514", "claude-haiku-4-5-20251001"]
479
+ },
480
+ gemini: {
481
+ name: "Google Gemini",
482
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
483
+ defaultModel: "gemini-2.5-flash",
484
+ keyPrefix: "AI",
485
+ keyEnvHint: "https://aistudio.google.com/apikey",
486
+ models: ["gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.5-pro", "gemini-3-flash-preview"]
487
+ },
488
+ ollama: {
489
+ name: "Ollama (local, free)",
490
+ baseURL: "http://localhost:11434/v1",
491
+ defaultModel: "llama3.1",
492
+ keyPrefix: "",
493
+ keyEnvHint: "No API key needed \u2014 install from https://ollama.ai",
494
+ models: ["llama3.1", "llama3.2", "mistral", "codestral", "qwen2.5", "deepseek-r1"]
495
+ }
496
+ };
497
+ var configSchema = z.object({
498
+ llm: z.object({
499
+ provider: z.enum(["openai", "anthropic", "gemini", "ollama"]).default("openai"),
500
+ baseURL: z.string().default("https://api.openai.com/v1"),
501
+ model: z.string().default("gpt-4o"),
502
+ apiKey: z.string().default(""),
503
+ temperature: z.number().min(0).max(2).default(0.1),
504
+ maxRetries: z.number().int().min(0).default(3)
505
+ }).default({}),
506
+ browser: z.object({
507
+ executablePath: z.string().default(""),
508
+ headless: z.boolean().default(true),
509
+ connectTimeout: z.number().default(30),
510
+ commandTimeout: z.number().default(60),
511
+ cdpEndpoint: z.string().default(""),
512
+ profile: z.string().default(""),
513
+ stealth: z.boolean().default(false)
514
+ }).default({}),
515
+ agent: z.object({
516
+ maxSteps: z.number().int().default(40),
517
+ stepDelay: z.number().default(0.4)
518
+ }).default({}),
519
+ domains: z.object({
520
+ allow: z.array(z.string()).default([]),
521
+ block: z.array(z.string()).default([]),
522
+ blockMessage: z.string().default("")
523
+ }).default({}),
524
+ output: z.object({
525
+ defaultFormat: z.enum(["table", "json", "yaml", "markdown", "csv"]).default("table"),
526
+ color: z.boolean().default(true)
527
+ }).default({})
528
+ });
529
+
530
+ // src/config/index.ts
531
+ var CONFIG_DIR = join(homedir(), ".lobster");
532
+ var CONFIG_FILE = join(CONFIG_DIR, "config.yaml");
533
+ function ensureConfigDir() {
534
+ if (!existsSync(CONFIG_DIR)) {
535
+ mkdirSync(CONFIG_DIR, { recursive: true });
536
+ }
537
+ }
538
+ function loadConfig() {
539
+ ensureConfigDir();
540
+ let fileConfig = {};
541
+ if (existsSync(CONFIG_FILE)) {
542
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
543
+ fileConfig = yaml.load(raw) || {};
544
+ }
545
+ const envOverrides = {};
546
+ if (process.env.LOBSTER_API_KEY) {
547
+ envOverrides.llm = { ...fileConfig.llm || {}, apiKey: process.env.LOBSTER_API_KEY };
548
+ }
549
+ if (process.env.LOBSTER_MODEL) {
550
+ envOverrides.llm = { ...envOverrides.llm || fileConfig.llm || {}, model: process.env.LOBSTER_MODEL };
551
+ }
552
+ if (process.env.LOBSTER_BASE_URL) {
553
+ envOverrides.llm = { ...envOverrides.llm || fileConfig.llm || {}, baseURL: process.env.LOBSTER_BASE_URL };
554
+ }
555
+ if (process.env.LOBSTER_CDP_ENDPOINT) {
556
+ envOverrides.browser = { ...fileConfig.browser || {}, cdpEndpoint: process.env.LOBSTER_CDP_ENDPOINT };
557
+ }
558
+ if (process.env.LOBSTER_BROWSER_PATH) {
559
+ envOverrides.browser = { ...envOverrides.browser || fileConfig.browser || {}, executablePath: process.env.LOBSTER_BROWSER_PATH };
560
+ }
561
+ const merged = { ...fileConfig, ...envOverrides };
562
+ return configSchema.parse(merged);
563
+ }
564
+ function saveConfig(config) {
565
+ ensureConfigDir();
566
+ const existing = loadConfig();
567
+ const merged = deepMerge(existing, config);
568
+ writeFileSync(CONFIG_FILE, yaml.dump(merged, { indent: 2 }), "utf-8");
569
+ }
570
+ function getConfigDir() {
571
+ return CONFIG_DIR;
572
+ }
573
+ function deepMerge(target, source) {
574
+ const result = { ...target };
575
+ for (const key of Object.keys(source)) {
576
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) {
577
+ result[key] = deepMerge(target[key], source[key]);
578
+ } else {
579
+ result[key] = source[key];
580
+ }
581
+ }
582
+ return result;
583
+ }
584
+
585
+ // src/browser/profiles.ts
586
+ var PROFILES_DIR = () => join2(getConfigDir(), "profiles");
587
+ var META_FILE = ".lobster-meta.json";
588
+ var VALID_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
589
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
590
+ "default",
591
+ "system",
592
+ "con",
593
+ "prn",
594
+ "aux",
595
+ "nul",
596
+ "com1",
597
+ "com2",
598
+ "com3",
599
+ "com4",
600
+ "com5",
601
+ "com6",
602
+ "com7",
603
+ "com8",
604
+ "com9",
605
+ "lpt1",
606
+ "lpt2",
607
+ "lpt3",
608
+ "lpt4",
609
+ "lpt5",
610
+ "lpt6",
611
+ "lpt7",
612
+ "lpt8",
613
+ "lpt9"
614
+ ]);
615
+ var CACHE_DIRS = [
616
+ "Cache",
617
+ "Code Cache",
618
+ "GPUCache",
619
+ "GrShaderCache",
620
+ "ShaderCache",
621
+ "Service Worker",
622
+ "Sessions",
623
+ "Session Storage",
624
+ "blob_storage"
625
+ ];
626
+ function ensureProfilesDir() {
627
+ const dir = PROFILES_DIR();
628
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
629
+ }
630
+ function validateName(name) {
631
+ if (!VALID_NAME.test(name)) {
632
+ throw new Error(`Invalid profile name "${name}". Use only letters, numbers, hyphens, underscores (max 64 chars).`);
633
+ }
634
+ if (RESERVED_NAMES.has(name.toLowerCase())) {
635
+ throw new Error(`"${name}" is a reserved name. Choose a different profile name.`);
636
+ }
637
+ }
638
+ function getProfileDir(name) {
639
+ return join2(PROFILES_DIR(), name);
640
+ }
641
+ function readMeta(profileDir) {
642
+ const metaPath = join2(profileDir, META_FILE);
643
+ if (!existsSync2(metaPath)) return null;
644
+ try {
645
+ return JSON.parse(readFileSync2(metaPath, "utf-8"));
646
+ } catch {
647
+ return null;
648
+ }
649
+ }
650
+ function writeMeta(profileDir, meta) {
651
+ writeFileSync2(join2(profileDir, META_FILE), JSON.stringify(meta, null, 2));
652
+ }
653
+ function getDirSizeMB(dirPath) {
654
+ let total = 0;
655
+ try {
656
+ const entries = readdirSync(dirPath, { withFileTypes: true });
657
+ for (const entry of entries) {
658
+ const fullPath = join2(dirPath, entry.name);
659
+ if (entry.isFile()) {
660
+ total += statSync(fullPath).size;
661
+ } else if (entry.isDirectory() && entry.name !== ".lobster-meta.json") {
662
+ total += getDirSizeMB(fullPath) * 1024 * 1024;
663
+ }
664
+ }
665
+ } catch {
666
+ }
667
+ return Math.round(total / (1024 * 1024) * 10) / 10;
668
+ }
669
+ function createProfile(name) {
670
+ validateName(name);
671
+ ensureProfilesDir();
672
+ const dir = getProfileDir(name);
673
+ if (existsSync2(dir)) {
674
+ throw new Error(`Profile "${name}" already exists.`);
675
+ }
676
+ mkdirSync2(dir, { recursive: true });
677
+ const meta = {
678
+ name,
679
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
680
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString()
681
+ };
682
+ writeMeta(dir, meta);
683
+ log.success(`Profile "${name}" created at ${dir}`);
684
+ return meta;
685
+ }
686
+ function listProfiles() {
687
+ ensureProfilesDir();
688
+ const dir = PROFILES_DIR();
689
+ const profiles = [];
690
+ try {
691
+ const entries = readdirSync(dir, { withFileTypes: true });
692
+ for (const entry of entries) {
693
+ if (!entry.isDirectory()) continue;
694
+ const profileDir = join2(dir, entry.name);
695
+ const meta = readMeta(profileDir);
696
+ if (meta) {
697
+ meta.sizeMB = getDirSizeMB(profileDir);
698
+ profiles.push(meta);
699
+ } else {
700
+ profiles.push({
701
+ name: entry.name,
702
+ createdAt: "unknown",
703
+ lastUsed: "unknown",
704
+ sizeMB: getDirSizeMB(profileDir)
705
+ });
706
+ }
707
+ }
708
+ } catch {
709
+ }
710
+ return profiles.sort((a, b) => a.name.localeCompare(b.name));
711
+ }
712
+ function removeProfile(name) {
713
+ const dir = getProfileDir(name);
714
+ if (!existsSync2(dir)) {
715
+ throw new Error(`Profile "${name}" does not exist.`);
716
+ }
717
+ rmSync(dir, { recursive: true, force: true });
718
+ log.success(`Profile "${name}" deleted.`);
719
+ }
720
+ function getProfileDataDir(name) {
721
+ validateName(name);
722
+ const dir = getProfileDir(name);
723
+ if (!existsSync2(dir)) {
724
+ createProfile(name);
725
+ } else {
726
+ const meta = readMeta(dir) || { name, createdAt: "unknown", lastUsed: "" };
727
+ meta.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
728
+ writeMeta(dir, meta);
729
+ }
730
+ return dir;
731
+ }
732
+ function resetProfileCache(name) {
733
+ const dir = getProfileDir(name);
734
+ if (!existsSync2(dir)) {
735
+ throw new Error(`Profile "${name}" does not exist.`);
736
+ }
737
+ let cleaned = 0;
738
+ for (const cacheDir of CACHE_DIRS) {
739
+ for (const base of [dir, join2(dir, "Default")]) {
740
+ const target = join2(base, cacheDir);
741
+ if (existsSync2(target)) {
742
+ rmSync(target, { recursive: true, force: true });
743
+ cleaned++;
744
+ }
745
+ }
746
+ }
747
+ log.success(`Profile "${name}" cache reset (${cleaned} directories cleaned).`);
748
+ }
749
+
750
+ // src/browser/chrome-attach.ts
751
+ import http from "http";
752
+ var DEFAULT_PORTS = [9222, 9229, 9333, 9515];
753
+ var PROBE_TIMEOUT = 1500;
754
+ function probePort(port) {
755
+ return new Promise((resolve) => {
756
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, {
757
+ timeout: PROBE_TIMEOUT
758
+ }, (res) => {
759
+ let data = "";
760
+ res.on("data", (chunk) => {
761
+ data += chunk;
762
+ });
763
+ res.on("end", () => {
764
+ try {
765
+ const info = JSON.parse(data);
766
+ if (info.webSocketDebuggerUrl) {
767
+ resolve({
768
+ wsEndpoint: info.webSocketDebuggerUrl,
769
+ port,
770
+ version: info["Protocol-Version"] || "",
771
+ browser: info.Browser || ""
772
+ });
773
+ } else {
774
+ resolve(null);
775
+ }
776
+ } catch {
777
+ resolve(null);
778
+ }
779
+ });
780
+ });
781
+ req.on("error", () => resolve(null));
782
+ req.on("timeout", () => {
783
+ req.destroy();
784
+ resolve(null);
785
+ });
786
+ });
787
+ }
788
+ async function discoverChrome(ports) {
789
+ const portsToCheck = ports || DEFAULT_PORTS;
790
+ log.debug(`Scanning ports for Chrome: ${portsToCheck.join(", ")}`);
791
+ const results = await Promise.all(portsToCheck.map(probePort));
792
+ const found = results.find(Boolean) || null;
793
+ if (found) {
794
+ log.info(`Found Chrome on port ${found.port}: ${found.browser}`);
795
+ } else {
796
+ log.debug("No running Chrome instance found on debug ports.");
797
+ }
798
+ return found;
799
+ }
800
+ async function getWebSocketDebuggerUrl(port) {
801
+ const result = await probePort(port);
802
+ return result?.wsEndpoint || null;
803
+ }
804
+ async function resolveAttachTarget(target) {
805
+ if (target === true || target === "true") {
806
+ const result = await discoverChrome();
807
+ if (!result) {
808
+ throw new Error(
809
+ "No running Chrome found. Start Chrome with:\n google-chrome --remote-debugging-port=9222\n # or on Mac:\n /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222"
810
+ );
811
+ }
812
+ return result.wsEndpoint;
813
+ }
814
+ if (typeof target === "string") {
815
+ if (target.startsWith("ws://") || target.startsWith("wss://")) {
816
+ return target;
817
+ }
818
+ const port = parseInt(target, 10);
819
+ if (!isNaN(port) && port > 0 && port < 65536) {
820
+ const url = await getWebSocketDebuggerUrl(port);
821
+ if (!url) {
822
+ throw new Error(`No Chrome found on port ${port}. Make sure Chrome is running with --remote-debugging-port=${port}`);
823
+ }
824
+ return url;
825
+ }
826
+ throw new Error(`Invalid attach target: "${target}". Use "true" for auto-discover, a port number, or a ws:// URL.`);
827
+ }
828
+ throw new Error("Invalid attach target.");
829
+ }
830
+
831
+ // src/browser/stealth.ts
832
+ var STEALTH_SCRIPT = `
833
+ (() => {
834
+ // \u2500\u2500 1. navigator.webdriver removal \u2500\u2500
835
+ // Most important: this is the #1 detection vector
836
+ Object.defineProperty(navigator, 'webdriver', {
837
+ get: () => undefined,
838
+ configurable: true,
839
+ });
840
+
841
+ // Also delete from prototype
842
+ delete Object.getPrototypeOf(navigator).webdriver;
843
+
844
+ // \u2500\u2500 2. CDP marker removal \u2500\u2500
845
+ // Chrome DevTools Protocol injects cdc_* properties on window
846
+ for (const key of Object.keys(window)) {
847
+ if (/^cdc_|^__webdriver|^__selenium|^__driver/.test(key)) {
848
+ try { delete window[key]; } catch {}
849
+ }
850
+ }
851
+
852
+ // \u2500\u2500 3. Chrome runtime spoofing \u2500\u2500
853
+ // Real Chrome has window.chrome with runtime, loadTimes, csi
854
+ if (!window.chrome) {
855
+ window.chrome = {};
856
+ }
857
+ if (!window.chrome.runtime) {
858
+ window.chrome.runtime = {
859
+ connect: function() {},
860
+ sendMessage: function() {},
861
+ onMessage: { addListener: function() {} },
862
+ id: undefined,
863
+ };
864
+ }
865
+ if (!window.chrome.loadTimes) {
866
+ window.chrome.loadTimes = function() {
867
+ return {
868
+ commitLoadTime: Date.now() / 1000 - 0.5,
869
+ connectionInfo: 'h2',
870
+ finishDocumentLoadTime: Date.now() / 1000 - 0.1,
871
+ finishLoadTime: Date.now() / 1000 - 0.05,
872
+ firstPaintAfterLoadTime: 0,
873
+ firstPaintTime: Date.now() / 1000 - 0.3,
874
+ navigationType: 'Other',
875
+ npnNegotiatedProtocol: 'h2',
876
+ requestTime: Date.now() / 1000 - 1,
877
+ startLoadTime: Date.now() / 1000 - 0.8,
878
+ wasAlternateProtocolAvailable: false,
879
+ wasFetchedViaSpdy: true,
880
+ wasNpnNegotiated: true,
881
+ };
882
+ };
883
+ }
884
+ if (!window.chrome.csi) {
885
+ window.chrome.csi = function() {
886
+ return {
887
+ onloadT: Date.now(),
888
+ startE: Date.now() - 500,
889
+ pageT: 500,
890
+ tran: 15,
891
+ };
892
+ };
893
+ }
894
+
895
+ // \u2500\u2500 4. Plugin array spoofing \u2500\u2500
896
+ // Headless Chrome reports empty plugins; real Chrome has at least 2
897
+ const fakePlugins = [
898
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
899
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
900
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
901
+ ];
902
+
903
+ Object.defineProperty(navigator, 'plugins', {
904
+ get: () => {
905
+ const arr = fakePlugins.map(p => {
906
+ const plugin = { ...p, item: (i) => plugin, namedItem: (n) => plugin };
907
+ return plugin;
908
+ });
909
+ arr.item = (i) => arr[i];
910
+ arr.namedItem = (n) => arr.find(p => p.name === n);
911
+ arr.refresh = () => {};
912
+ return arr;
913
+ },
914
+ });
915
+
916
+ // \u2500\u2500 5. Languages \u2500\u2500
917
+ Object.defineProperty(navigator, 'languages', {
918
+ get: () => ['en-US', 'en'],
919
+ });
920
+ Object.defineProperty(navigator, 'language', {
921
+ get: () => 'en-US',
922
+ });
923
+
924
+ // \u2500\u2500 6. Platform consistency \u2500\u2500
925
+ // Ensure platform matches user agent
926
+ const platform = navigator.userAgent.includes('Mac') ? 'MacIntel' :
927
+ navigator.userAgent.includes('Win') ? 'Win32' :
928
+ navigator.userAgent.includes('Linux') ? 'Linux x86_64' : navigator.platform;
929
+ Object.defineProperty(navigator, 'platform', { get: () => platform });
930
+
931
+ // \u2500\u2500 7. Hardware concurrency & device memory \u2500\u2500
932
+ // Headless often reports unusual values
933
+ if (navigator.hardwareConcurrency < 2) {
934
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
935
+ }
936
+ if (!navigator.deviceMemory || navigator.deviceMemory < 2) {
937
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
938
+ }
939
+
940
+ // \u2500\u2500 8. WebGL vendor/renderer spoofing \u2500\u2500
941
+ // Headless reports "Google SwiftShader" which is a dead giveaway
942
+ const origGetParameter = WebGLRenderingContext.prototype.getParameter;
943
+ WebGLRenderingContext.prototype.getParameter = function(param) {
944
+ // UNMASKED_VENDOR_WEBGL
945
+ if (param === 0x9245) return 'Intel Inc.';
946
+ // UNMASKED_RENDERER_WEBGL
947
+ if (param === 0x9246) return 'Intel Iris OpenGL Engine';
948
+ return origGetParameter.call(this, param);
949
+ };
950
+
951
+ // Also for WebGL2
952
+ if (typeof WebGL2RenderingContext !== 'undefined') {
953
+ const origGetParameter2 = WebGL2RenderingContext.prototype.getParameter;
954
+ WebGL2RenderingContext.prototype.getParameter = function(param) {
955
+ if (param === 0x9245) return 'Intel Inc.';
956
+ if (param === 0x9246) return 'Intel Iris OpenGL Engine';
957
+ return origGetParameter2.call(this, param);
958
+ };
959
+ }
960
+
961
+ // \u2500\u2500 9. Canvas fingerprint noise \u2500\u2500
962
+ // Adds subtle deterministic noise to canvas output based on domain
963
+ const seed = location.hostname.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
964
+ const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
965
+ HTMLCanvasElement.prototype.toDataURL = function(type) {
966
+ const ctx = this.getContext('2d');
967
+ if (ctx && this.width > 0 && this.height > 0) {
968
+ try {
969
+ const imageData = ctx.getImageData(0, 0, 1, 1);
970
+ // Flip a single pixel with seeded noise
971
+ imageData.data[0] = (imageData.data[0] + seed) % 256;
972
+ ctx.putImageData(imageData, 0, 0);
973
+ } catch {}
974
+ }
975
+ return origToDataURL.apply(this, arguments);
976
+ };
977
+
978
+ // \u2500\u2500 10. Permissions API \u2500\u2500
979
+ // Headless returns 'denied' for notifications; real Chrome returns 'prompt'
980
+ const origQuery = navigator.permissions?.query?.bind(navigator.permissions);
981
+ if (origQuery) {
982
+ navigator.permissions.query = function(descriptor) {
983
+ if (descriptor.name === 'notifications') {
984
+ return Promise.resolve({ state: Notification.permission || 'prompt', onchange: null });
985
+ }
986
+ return origQuery(descriptor);
987
+ };
988
+ }
989
+
990
+ // \u2500\u2500 11. Notification constructor \u2500\u2500
991
+ if (!window.Notification) {
992
+ window.Notification = function() {};
993
+ window.Notification.permission = 'default';
994
+ window.Notification.requestPermission = () => Promise.resolve('default');
995
+ }
996
+
997
+ // \u2500\u2500 12. Connection type \u2500\u2500
998
+ if (navigator.connection) {
999
+ Object.defineProperty(navigator.connection, 'rtt', { get: () => 50 });
1000
+ }
1001
+ })()
1002
+ `;
1003
+ async function injectStealth(page) {
1004
+ await page.evaluateOnNewDocument(STEALTH_SCRIPT);
1005
+ }
1006
+ var STEALTH_ARGS = [
1007
+ "--disable-blink-features=AutomationControlled",
1008
+ "--disable-features=IsolateOrigins,site-per-process",
1009
+ "--disable-infobars",
1010
+ "--window-size=1920,1080"
1011
+ ];
1012
+
451
1013
  // src/browser/manager.ts
452
1014
  var BrowserManager = class {
453
1015
  browser = null;
454
1016
  config;
1017
+ isAttached = false;
455
1018
  constructor(config = {}) {
456
1019
  this.config = config;
457
1020
  }
458
1021
  async connect() {
459
1022
  if (this.browser?.connected) return this.browser;
1023
+ if (this.config.attach) {
1024
+ const wsEndpoint = await resolveAttachTarget(this.config.attach);
1025
+ log.info(`Attaching to Chrome: ${wsEndpoint}`);
1026
+ this.browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
1027
+ this.isAttached = true;
1028
+ return this.browser;
1029
+ }
460
1030
  if (this.config.cdpEndpoint) {
461
1031
  log.debug(`Connecting to CDP endpoint: ${this.config.cdpEndpoint}`);
462
1032
  this.browser = await puppeteer.connect({
463
1033
  browserWSEndpoint: this.config.cdpEndpoint
464
1034
  });
1035
+ this.isAttached = true;
465
1036
  return this.browser;
466
1037
  }
467
1038
  const executablePath = this.config.executablePath || findChrome();
@@ -470,27 +1041,48 @@ var BrowserManager = class {
470
1041
  "Chrome/Chromium not found. Set LOBSTER_BROWSER_PATH or config browser.executablePath"
471
1042
  );
472
1043
  }
1044
+ const args = [
1045
+ "--no-sandbox",
1046
+ "--disable-setuid-sandbox",
1047
+ "--disable-dev-shm-usage",
1048
+ "--disable-gpu"
1049
+ ];
1050
+ if (this.config.stealth) {
1051
+ args.push(...STEALTH_ARGS);
1052
+ }
1053
+ let userDataDir;
1054
+ if (this.config.profile) {
1055
+ userDataDir = getProfileDataDir(this.config.profile);
1056
+ log.info(`Using profile "${this.config.profile}" \u2192 ${userDataDir}`);
1057
+ }
473
1058
  log.debug(`Launching Chrome: ${executablePath}`);
474
1059
  this.browser = await puppeteer.launch({
475
1060
  executablePath,
476
1061
  headless: this.config.headless ?? true,
477
- args: [
478
- "--no-sandbox",
479
- "--disable-setuid-sandbox",
480
- "--disable-dev-shm-usage",
481
- "--disable-gpu"
482
- ]
1062
+ userDataDir,
1063
+ args
483
1064
  });
1065
+ this.isAttached = false;
484
1066
  return this.browser;
485
1067
  }
486
1068
  async newPage() {
487
1069
  const browser = await this.connect();
488
- return browser.newPage();
1070
+ const page = await browser.newPage();
1071
+ if (this.config.stealth) {
1072
+ await injectStealth(page);
1073
+ log.debug("Stealth mode enabled");
1074
+ }
1075
+ return page;
489
1076
  }
490
1077
  async close() {
491
1078
  if (this.browser) {
492
- await this.browser.close().catch(() => {
493
- });
1079
+ if (this.isAttached) {
1080
+ this.browser.disconnect();
1081
+ log.debug("Disconnected from Chrome (attached mode)");
1082
+ } else {
1083
+ await this.browser.close().catch(() => {
1084
+ });
1085
+ }
494
1086
  this.browser = null;
495
1087
  }
496
1088
  }
@@ -510,7 +1102,7 @@ function findChrome() {
510
1102
  "/usr/bin/chromium",
511
1103
  "/snap/bin/chromium"
512
1104
  ];
513
- return paths.find((p) => existsSync(p));
1105
+ return paths.find((p) => existsSync3(p));
514
1106
  }
515
1107
 
516
1108
  // src/browser/dom/flat-tree.ts
@@ -1016,6 +1608,164 @@ var SNAPSHOT_SCRIPT = `
1016
1608
  })()
1017
1609
  `;
1018
1610
 
1611
+ // src/browser/dom/compact-snapshot.ts
1612
+ var COMPACT_SNAPSHOT_SCRIPT = `
1613
+ (() => {
1614
+ const TOKEN_BUDGET = 800;
1615
+ const CHARS_PER_TOKEN = 4;
1616
+
1617
+ const INTERACTIVE_TAGS = new Set([
1618
+ 'a','button','input','select','textarea','details','summary','label',
1619
+ ]);
1620
+ const INTERACTIVE_ROLES = new Set([
1621
+ 'button','link','textbox','checkbox','radio','combobox','listbox',
1622
+ 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',
1623
+ 'option','menuitemcheckbox','menuitemradio','treeitem',
1624
+ ]);
1625
+ const LANDMARK_TAGS = new Map([
1626
+ ['nav', 'Navigation'],
1627
+ ['main', 'Main Content'],
1628
+ ['header', 'Header'],
1629
+ ['footer', 'Footer'],
1630
+ ['aside', 'Sidebar'],
1631
+ ['form', 'Form'],
1632
+ ]);
1633
+ const LANDMARK_ROLES = new Map([
1634
+ ['navigation', 'Navigation'],
1635
+ ['main', 'Main Content'],
1636
+ ['banner', 'Header'],
1637
+ ['contentinfo', 'Footer'],
1638
+ ['complementary', 'Sidebar'],
1639
+ ['search', 'Search'],
1640
+ ['dialog', 'Dialog'],
1641
+ ]);
1642
+
1643
+ function isVisible(el) {
1644
+ if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;
1645
+ const s = getComputedStyle(el);
1646
+ return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
1647
+ }
1648
+
1649
+ function isInteractive(el) {
1650
+ const tag = el.tagName.toLowerCase();
1651
+ if (INTERACTIVE_TAGS.has(tag)) {
1652
+ if (el.disabled) return false;
1653
+ if (tag === 'input' && el.type === 'hidden') return false;
1654
+ return true;
1655
+ }
1656
+ const role = el.getAttribute('role');
1657
+ if (role && INTERACTIVE_ROLES.has(role)) return true;
1658
+ if (el.contentEditable === 'true') return true;
1659
+ if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;
1660
+ return false;
1661
+ }
1662
+
1663
+ function getRole(el) {
1664
+ const role = el.getAttribute('role');
1665
+ if (role) return role;
1666
+ const tag = el.tagName.toLowerCase();
1667
+ if (tag === 'a') return 'link';
1668
+ if (tag === 'button' || tag === 'summary') return 'button';
1669
+ if (tag === 'input') return el.type || 'text';
1670
+ if (tag === 'select') return 'select';
1671
+ if (tag === 'textarea') return 'textarea';
1672
+ if (tag === 'label') return 'label';
1673
+ return tag;
1674
+ }
1675
+
1676
+ function getName(el) {
1677
+ return (
1678
+ el.getAttribute('aria-label') ||
1679
+ el.getAttribute('alt') ||
1680
+ el.getAttribute('title') ||
1681
+ el.getAttribute('placeholder') ||
1682
+ (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button') ? el.value : '') ||
1683
+ (el.id ? document.querySelector('label[for="' + el.id + '"]')?.textContent?.trim() : '') ||
1684
+ (el.children.length <= 2 ? el.textContent?.trim() : '') ||
1685
+ ''
1686
+ ).slice(0, 60);
1687
+ }
1688
+
1689
+ function getValue(el) {
1690
+ const tag = el.tagName.toLowerCase();
1691
+ if (tag === 'input') {
1692
+ const type = el.type || 'text';
1693
+ if (type === 'checkbox' || type === 'radio') return el.checked ? 'checked' : 'unchecked';
1694
+ if (type === 'password') return el.value ? '****' : '';
1695
+ return el.value ? el.value.slice(0, 30) : '';
1696
+ }
1697
+ if (tag === 'textarea') return el.value ? el.value.slice(0, 30) : '';
1698
+ if (tag === 'select' && el.selectedOptions?.length) return el.selectedOptions[0].text.slice(0, 30);
1699
+ return '';
1700
+ }
1701
+
1702
+ // Collect elements
1703
+ let idx = 0;
1704
+ let charsUsed = 0;
1705
+ const lines = [];
1706
+ let lastLandmark = '';
1707
+
1708
+ // Page header
1709
+ const scrollY = window.scrollY;
1710
+ const scrollMax = document.documentElement.scrollHeight - window.innerHeight;
1711
+ const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;
1712
+ const header = 'url: ' + location.href + ' | scroll: ' + scrollPct + '%';
1713
+ lines.push(header);
1714
+ charsUsed += header.length;
1715
+
1716
+ // Walk DOM
1717
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
1718
+ let node;
1719
+ while ((node = walker.nextNode())) {
1720
+ if (!isVisible(node)) continue;
1721
+
1722
+ const tag = node.tagName.toLowerCase();
1723
+ if (['script','style','noscript','svg','path','meta','link','head','template'].includes(tag)) continue;
1724
+
1725
+ // Check for landmark
1726
+ const role = node.getAttribute('role');
1727
+ const landmark = LANDMARK_TAGS.get(tag) || (role ? LANDMARK_ROLES.get(role) : null);
1728
+ if (landmark && landmark !== lastLandmark) {
1729
+ const sectionLine = '--- ' + landmark + ' ---';
1730
+ if (charsUsed + sectionLine.length > TOKEN_BUDGET * CHARS_PER_TOKEN) break;
1731
+ lines.push(sectionLine);
1732
+ charsUsed += sectionLine.length;
1733
+ lastLandmark = landmark;
1734
+ }
1735
+
1736
+ // Only emit interactive elements
1737
+ if (!isInteractive(node)) continue;
1738
+
1739
+ const elRole = getRole(node);
1740
+ const name = getName(node);
1741
+ const value = getValue(node);
1742
+
1743
+ // Build compact line
1744
+ let line = '[' + idx + '] ' + elRole;
1745
+ if (name) line += ' "' + name.replace(/"/g, "'") + '"';
1746
+ if (value) line += ' val="' + value.replace(/"/g, "'") + '"';
1747
+
1748
+ // Check token budget
1749
+ if (charsUsed + line.length > TOKEN_BUDGET * CHARS_PER_TOKEN) {
1750
+ lines.push('... (' + (document.querySelectorAll('a,button,input,select,textarea,[role]').length - idx) + ' more elements)');
1751
+ break;
1752
+ }
1753
+
1754
+ // Annotate element with ref for clicking
1755
+ try { node.dataset.ref = String(idx); } catch {}
1756
+
1757
+ lines.push(line);
1758
+ charsUsed += line.length;
1759
+ idx++;
1760
+ }
1761
+
1762
+ return lines.join('\\n');
1763
+ })()
1764
+ `;
1765
+ function buildCompactSnapshotScript(tokenBudget = 800) {
1766
+ return COMPACT_SNAPSHOT_SCRIPT.replace("const TOKEN_BUDGET = 800;", `const TOKEN_BUDGET = ${tokenBudget};`);
1767
+ }
1768
+
1019
1769
  // src/browser/dom/semantic-tree.ts
1020
1770
  var SEMANTIC_TREE_SCRIPT = `
1021
1771
  (() => {
@@ -1526,18 +2276,76 @@ var FORM_STATE_SCRIPT = `
1526
2276
  });
1527
2277
  }
1528
2278
 
1529
- // Collect orphan fields (not in a <form>)
1530
- const allInputs = document.querySelectorAll(
1531
- 'input, textarea, select, [contenteditable="true"]'
1532
- );
1533
- for (const el of allInputs) {
1534
- if (!el.form) {
1535
- const field = extractField(el);
1536
- if (field) result.orphanFields.push(field);
1537
- }
1538
- }
1539
-
1540
- return result;
2279
+ // Collect orphan fields (not in a <form>)
2280
+ const allInputs = document.querySelectorAll(
2281
+ 'input, textarea, select, [contenteditable="true"]'
2282
+ );
2283
+ for (const el of allInputs) {
2284
+ if (!el.form) {
2285
+ const field = extractField(el);
2286
+ if (field) result.orphanFields.push(field);
2287
+ }
2288
+ }
2289
+
2290
+ return result;
2291
+ })()
2292
+ `;
2293
+
2294
+ // src/browser/dom/interactive.ts
2295
+ var INTERACTIVE_ELEMENTS_SCRIPT = `
2296
+ (() => {
2297
+ const results = [];
2298
+
2299
+ function classify(el) {
2300
+ const tag = el.tagName.toLowerCase();
2301
+ const role = el.getAttribute('role');
2302
+ const types = [];
2303
+
2304
+ // Native interactive
2305
+ if (['a', 'button', 'input', 'select', 'textarea', 'details', 'summary'].includes(tag)) {
2306
+ types.push('native');
2307
+ }
2308
+
2309
+ // ARIA role interactive
2310
+ if (role && ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'tab', 'switch', 'menuitem', 'slider'].includes(role)) {
2311
+ types.push('aria');
2312
+ }
2313
+
2314
+ // Contenteditable
2315
+ if (el.contentEditable === 'true') types.push('contenteditable');
2316
+
2317
+ // Focusable
2318
+ if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) types.push('focusable');
2319
+
2320
+ // Has click listener (approximate)
2321
+ if (el.onclick) types.push('listener');
2322
+
2323
+ return types;
2324
+ }
2325
+
2326
+ let idx = 0;
2327
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
2328
+ let node;
2329
+ while (node = walker.nextNode()) {
2330
+ const types = classify(node);
2331
+ if (types.length === 0) continue;
2332
+
2333
+ const style = getComputedStyle(node);
2334
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
2335
+
2336
+ const rect = node.getBoundingClientRect();
2337
+ results.push({
2338
+ index: idx++,
2339
+ tag: node.tagName.toLowerCase(),
2340
+ role: node.getAttribute('role') || '',
2341
+ text: (node.textContent || '').trim().slice(0, 100),
2342
+ types,
2343
+ ariaLabel: node.getAttribute('aria-label') || '',
2344
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
2345
+ });
2346
+ }
2347
+
2348
+ return results;
1541
2349
  })()
1542
2350
  `;
1543
2351
 
@@ -1597,6 +2405,155 @@ var GET_INTERCEPTED_SCRIPT = `
1597
2405
  })()
1598
2406
  `;
1599
2407
 
2408
+ // src/browser/semantic-find.ts
2409
+ var SYNONYMS = {
2410
+ btn: ["button"],
2411
+ button: ["btn", "submit", "click"],
2412
+ submit: ["go", "send", "ok", "confirm", "done", "button"],
2413
+ search: ["find", "lookup", "query", "filter"],
2414
+ login: ["signin", "sign-in", "log-in", "authenticate"],
2415
+ signup: ["register", "create-account", "sign-up", "join"],
2416
+ logout: ["signout", "sign-out", "log-out"],
2417
+ close: ["dismiss", "x", "cancel", "exit"],
2418
+ menu: ["nav", "navigation", "hamburger", "sidebar"],
2419
+ nav: ["navigation", "menu", "navbar"],
2420
+ input: ["field", "textbox", "text", "entry"],
2421
+ email: ["mail", "e-mail"],
2422
+ password: ["pass", "pwd", "secret"],
2423
+ next: ["continue", "forward", "proceed"],
2424
+ back: ["previous", "return", "go-back"],
2425
+ save: ["store", "keep", "persist"],
2426
+ delete: ["remove", "trash", "discard", "destroy"],
2427
+ edit: ["modify", "change", "update"],
2428
+ add: ["create", "new", "plus", "insert"],
2429
+ settings: ["preferences", "config", "options", "gear"],
2430
+ profile: ["account", "user", "avatar"],
2431
+ home: ["main", "dashboard", "start"],
2432
+ link: ["anchor", "href", "url"],
2433
+ select: ["dropdown", "combo", "picker", "choose"],
2434
+ checkbox: ["check", "toggle", "tick"],
2435
+ upload: ["attach", "file", "browse"],
2436
+ download: ["save", "export"]
2437
+ };
2438
+ var ROLE_KEYWORDS = /* @__PURE__ */ new Set([
2439
+ "button",
2440
+ "link",
2441
+ "input",
2442
+ "textbox",
2443
+ "checkbox",
2444
+ "radio",
2445
+ "select",
2446
+ "dropdown",
2447
+ "tab",
2448
+ "menu",
2449
+ "menuitem",
2450
+ "switch",
2451
+ "slider",
2452
+ "combobox",
2453
+ "searchbox",
2454
+ "option"
2455
+ ]);
2456
+ function tokenize(text) {
2457
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((t) => t.length > 0);
2458
+ }
2459
+ function expandSynonyms(tokens) {
2460
+ const expanded = new Set(tokens);
2461
+ for (const token of tokens) {
2462
+ const syns = SYNONYMS[token];
2463
+ if (syns) {
2464
+ for (const syn of syns) expanded.add(syn);
2465
+ }
2466
+ }
2467
+ return expanded;
2468
+ }
2469
+ function freqMap(tokens) {
2470
+ const map = /* @__PURE__ */ new Map();
2471
+ for (const t of tokens) {
2472
+ map.set(t, (map.get(t) || 0) + 1);
2473
+ }
2474
+ return map;
2475
+ }
2476
+ function jaccardScore(queryTokens, descTokens) {
2477
+ const qFreq = freqMap(queryTokens);
2478
+ const dFreq = freqMap(descTokens);
2479
+ let intersection = 0;
2480
+ let union = 0;
2481
+ const allTokens = /* @__PURE__ */ new Set([...qFreq.keys(), ...dFreq.keys()]);
2482
+ for (const token of allTokens) {
2483
+ const qCount = qFreq.get(token) || 0;
2484
+ const dCount = dFreq.get(token) || 0;
2485
+ intersection += Math.min(qCount, dCount);
2486
+ union += Math.max(qCount, dCount);
2487
+ }
2488
+ return union === 0 ? 0 : intersection / union;
2489
+ }
2490
+ function prefixScore(queryTokens, descTokens) {
2491
+ if (queryTokens.length === 0 || descTokens.length === 0) return 0;
2492
+ let matches = 0;
2493
+ for (const qt of queryTokens) {
2494
+ if (qt.length < 3) continue;
2495
+ for (const dt of descTokens) {
2496
+ if (dt.startsWith(qt) || qt.startsWith(dt)) {
2497
+ matches += 0.5;
2498
+ break;
2499
+ }
2500
+ }
2501
+ }
2502
+ return Math.min(matches / queryTokens.length, 0.3);
2503
+ }
2504
+ function roleBoost(queryTokens, elementRole) {
2505
+ const roleLower = elementRole.toLowerCase();
2506
+ for (const qt of queryTokens) {
2507
+ if (ROLE_KEYWORDS.has(qt) && roleLower.includes(qt)) {
2508
+ return 0.2;
2509
+ }
2510
+ }
2511
+ return 0;
2512
+ }
2513
+ function scoreElement(queryTokens, queryExpanded, element) {
2514
+ const descParts = [
2515
+ element.text,
2516
+ element.role,
2517
+ element.tag,
2518
+ element.ariaLabel
2519
+ ].filter(Boolean);
2520
+ const descText = descParts.join(" ");
2521
+ const descTokens = tokenize(descText);
2522
+ if (descTokens.length === 0) return 0;
2523
+ const descExpanded = expandSynonyms(descTokens);
2524
+ const expandedQueryTokens = [...queryExpanded];
2525
+ const expandedDescTokens = [...descExpanded];
2526
+ const jaccard = jaccardScore(expandedQueryTokens, expandedDescTokens);
2527
+ const prefix = prefixScore(queryTokens, descTokens);
2528
+ const role = roleBoost(queryTokens, element.role || element.tag);
2529
+ const queryStr = queryTokens.join(" ");
2530
+ const descStr = descTokens.join(" ");
2531
+ const exactBonus = descStr.includes(queryStr) ? 0.3 : 0;
2532
+ return Math.min(jaccard + prefix + role + exactBonus, 1);
2533
+ }
2534
+ function semanticFind(elements, query, options) {
2535
+ const maxResults = options?.maxResults ?? 5;
2536
+ const minScore = options?.minScore ?? 0.3;
2537
+ const queryTokens = tokenize(query);
2538
+ if (queryTokens.length === 0) return [];
2539
+ const queryExpanded = expandSynonyms(queryTokens);
2540
+ const scored = [];
2541
+ for (const el of elements) {
2542
+ const score = scoreElement(queryTokens, queryExpanded, el);
2543
+ if (score >= minScore) {
2544
+ scored.push({
2545
+ ref: el.index,
2546
+ score: Math.round(score * 100) / 100,
2547
+ text: (el.text || el.ariaLabel || "").slice(0, 60),
2548
+ role: el.role || el.tag,
2549
+ tag: el.tag
2550
+ });
2551
+ }
2552
+ }
2553
+ scored.sort((a, b) => b.score - a.score);
2554
+ return scored.slice(0, maxResults);
2555
+ }
2556
+
1600
2557
  // src/browser/page-adapter.ts
1601
2558
  var PuppeteerPage = class {
1602
2559
  page;
@@ -1624,7 +2581,10 @@ var PuppeteerPage = class {
1624
2581
  async evaluate(js) {
1625
2582
  return this.page.evaluate(js);
1626
2583
  }
1627
- async snapshot(_opts) {
2584
+ async snapshot(opts) {
2585
+ if (opts?.compact) {
2586
+ return this.page.evaluate(COMPACT_SNAPSHOT_SCRIPT);
2587
+ }
1628
2588
  return this.page.evaluate(SNAPSHOT_SCRIPT);
1629
2589
  }
1630
2590
  async semanticTree(_opts) {
@@ -1896,69 +2856,15 @@ var PuppeteerPage = class {
1896
2856
  active: p === this.page
1897
2857
  }));
1898
2858
  }
2859
+ async find(query, options) {
2860
+ const elements = await this.page.evaluate(INTERACTIVE_ELEMENTS_SCRIPT);
2861
+ return semanticFind(elements, query, options);
2862
+ }
1899
2863
  async close() {
1900
2864
  await this.page.close();
1901
2865
  }
1902
2866
  };
1903
2867
 
1904
- // src/browser/dom/interactive.ts
1905
- var INTERACTIVE_ELEMENTS_SCRIPT = `
1906
- (() => {
1907
- const results = [];
1908
-
1909
- function classify(el) {
1910
- const tag = el.tagName.toLowerCase();
1911
- const role = el.getAttribute('role');
1912
- const types = [];
1913
-
1914
- // Native interactive
1915
- if (['a', 'button', 'input', 'select', 'textarea', 'details', 'summary'].includes(tag)) {
1916
- types.push('native');
1917
- }
1918
-
1919
- // ARIA role interactive
1920
- if (role && ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'tab', 'switch', 'menuitem', 'slider'].includes(role)) {
1921
- types.push('aria');
1922
- }
1923
-
1924
- // Contenteditable
1925
- if (el.contentEditable === 'true') types.push('contenteditable');
1926
-
1927
- // Focusable
1928
- if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) types.push('focusable');
1929
-
1930
- // Has click listener (approximate)
1931
- if (el.onclick) types.push('listener');
1932
-
1933
- return types;
1934
- }
1935
-
1936
- let idx = 0;
1937
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
1938
- let node;
1939
- while (node = walker.nextNode()) {
1940
- const types = classify(node);
1941
- if (types.length === 0) continue;
1942
-
1943
- const style = getComputedStyle(node);
1944
- if (style.display === 'none' || style.visibility === 'hidden') continue;
1945
-
1946
- const rect = node.getBoundingClientRect();
1947
- results.push({
1948
- index: idx++,
1949
- tag: node.tagName.toLowerCase(),
1950
- role: node.getAttribute('role') || '',
1951
- text: (node.textContent || '').trim().slice(0, 100),
1952
- types,
1953
- ariaLabel: node.getAttribute('aria-label') || '',
1954
- rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1955
- });
1956
- }
1957
-
1958
- return results;
1959
- })()
1960
- `;
1961
-
1962
2868
  // src/browser/lightpanda.ts
1963
2869
  var SELF_CLOSING = /* @__PURE__ */ new Set([
1964
2870
  "area",
@@ -2672,8 +3578,8 @@ function getAllSites() {
2672
3578
  }
2673
3579
 
2674
3580
  // src/discover/explore.ts
2675
- import { writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
2676
- import { join } from "path";
3581
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
3582
+ import { join as join3 } from "path";
2677
3583
  var SITE_ALIASES = {
2678
3584
  "x.com": "twitter",
2679
3585
  "twitter.com": "twitter",
@@ -2921,8 +3827,8 @@ async function recoverMissingBodies(page, endpoints) {
2921
3827
  }
2922
3828
  }
2923
3829
  function writeArtifacts(dir, result) {
2924
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
2925
- writeFileSync(join(dir, "manifest.json"), JSON.stringify({
3830
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
3831
+ writeFileSync3(join3(dir, "manifest.json"), JSON.stringify({
2926
3832
  site: result.site,
2927
3833
  domain: result.domain,
2928
3834
  framework: result.framework,
@@ -2931,7 +3837,7 @@ function writeArtifacts(dir, result) {
2931
3837
  endpointCount: result.endpoints.length,
2932
3838
  exploredAt: (/* @__PURE__ */ new Date()).toISOString()
2933
3839
  }, null, 2));
2934
- writeFileSync(join(dir, "endpoints.json"), JSON.stringify(
3840
+ writeFileSync3(join3(dir, "endpoints.json"), JSON.stringify(
2935
3841
  result.endpoints.map((ep) => ({
2936
3842
  url: ep.url,
2937
3843
  pattern: ep.pattern,
@@ -2948,7 +3854,7 @@ function writeArtifacts(dir, result) {
2948
3854
  null,
2949
3855
  2
2950
3856
  ));
2951
- writeFileSync(join(dir, "capabilities.json"), JSON.stringify(
3857
+ writeFileSync3(join3(dir, "capabilities.json"), JSON.stringify(
2952
3858
  result.capabilities.map((cap) => {
2953
3859
  const matchingEndpoints = result.endpoints.filter((ep) => {
2954
3860
  const path = ep.url.toLowerCase();
@@ -2976,9 +3882,9 @@ function writeArtifacts(dir, result) {
2976
3882
  authSummary[ind].push(ep.pattern);
2977
3883
  }
2978
3884
  }
2979
- writeFileSync(join(dir, "auth.json"), JSON.stringify(authSummary, null, 2));
3885
+ writeFileSync3(join3(dir, "auth.json"), JSON.stringify(authSummary, null, 2));
2980
3886
  if (result.stores && result.stores.length > 0) {
2981
- writeFileSync(join(dir, "stores.json"), JSON.stringify(result.stores, null, 2));
3887
+ writeFileSync3(join3(dir, "stores.json"), JSON.stringify(result.stores, null, 2));
2982
3888
  }
2983
3889
  log.success(`Artifacts written to ${dir}/`);
2984
3890
  }
@@ -3115,14 +4021,14 @@ async function exploreSite(page, url, options) {
3115
4021
  stores: stores.length > 0 ? stores : void 0,
3116
4022
  capabilities
3117
4023
  };
3118
- const outputDir = options?.outputDir || join(process.cwd(), ".lobster", "explore", site);
4024
+ const outputDir = options?.outputDir || join3(process.cwd(), ".lobster", "explore", site);
3119
4025
  writeArtifacts(outputDir, result);
3120
4026
  result.artifactDir = outputDir;
3121
4027
  return result;
3122
4028
  }
3123
4029
 
3124
4030
  // src/discover/synthesize.ts
3125
- import yaml from "js-yaml";
4031
+ import yaml2 from "js-yaml";
3126
4032
  function synthesizeAdapter(result, goal) {
3127
4033
  const topEndpoints = result.endpoints.filter((e) => e.score > 0).slice(0, 3);
3128
4034
  if (topEndpoints.length === 0) {
@@ -3193,7 +4099,7 @@ function synthesizeAdapter(result, goal) {
3193
4099
  pipeline,
3194
4100
  columns: columns.length > 0 ? columns : void 0
3195
4101
  };
3196
- return yaml.dump(adapter, { indent: 2, lineWidth: 120 });
4102
+ return yaml2.dump(adapter, { indent: 2, lineWidth: 120 });
3197
4103
  }
3198
4104
 
3199
4105
  // src/cascade/index.ts
@@ -3362,17 +4268,17 @@ function makeRoutingDecision(request) {
3362
4268
  }
3363
4269
 
3364
4270
  // src/agent/core.ts
3365
- import { readFileSync } from "fs";
3366
- import { join as join2, dirname } from "path";
4271
+ import { readFileSync as readFileSync3 } from "fs";
4272
+ import { join as join4, dirname } from "path";
3367
4273
  import { fileURLToPath } from "url";
3368
4274
 
3369
4275
  // src/agent/tools/click.ts
3370
- import { z } from "zod";
4276
+ import { z as z2 } from "zod";
3371
4277
  function createClickTool(page) {
3372
4278
  return {
3373
4279
  description: "Click on an interactive element by its index number from the page content.",
3374
- inputSchema: z.object({
3375
- index: z.number().describe("The index of the element to click")
4280
+ inputSchema: z2.object({
4281
+ index: z2.number().describe("The index of the element to click")
3376
4282
  }),
3377
4283
  execute: async (args) => {
3378
4284
  await page.click(args.index);
@@ -3382,13 +4288,13 @@ function createClickTool(page) {
3382
4288
  }
3383
4289
 
3384
4290
  // src/agent/tools/type.ts
3385
- import { z as z2 } from "zod";
4291
+ import { z as z3 } from "zod";
3386
4292
  function createTypeTool(page) {
3387
4293
  return {
3388
4294
  description: "Type text into an input field identified by its index number.",
3389
- inputSchema: z2.object({
3390
- index: z2.number().describe("The index of the input element"),
3391
- text: z2.string().describe("The text to type")
4295
+ inputSchema: z3.object({
4296
+ index: z3.number().describe("The index of the input element"),
4297
+ text: z3.string().describe("The text to type")
3392
4298
  }),
3393
4299
  execute: async (args) => {
3394
4300
  await page.typeText(args.index, args.text);
@@ -3398,13 +4304,13 @@ function createTypeTool(page) {
3398
4304
  }
3399
4305
 
3400
4306
  // src/agent/tools/scroll.ts
3401
- import { z as z3 } from "zod";
4307
+ import { z as z4 } from "zod";
3402
4308
  function createScrollTool(page) {
3403
4309
  return {
3404
4310
  description: "Scroll the page in a given direction. Use to reveal more content.",
3405
- inputSchema: z3.object({
3406
- direction: z3.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
3407
- amount: z3.number().optional().describe("Pixels to scroll (default 500)")
4311
+ inputSchema: z4.object({
4312
+ direction: z4.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
4313
+ amount: z4.number().optional().describe("Pixels to scroll (default 500)")
3408
4314
  }),
3409
4315
  execute: async (args) => {
3410
4316
  await page.scroll(args.direction, args.amount);
@@ -3414,13 +4320,13 @@ function createScrollTool(page) {
3414
4320
  }
3415
4321
 
3416
4322
  // src/agent/tools/select.ts
3417
- import { z as z4 } from "zod";
4323
+ import { z as z5 } from "zod";
3418
4324
  function createSelectTool(page) {
3419
4325
  return {
3420
4326
  description: "Select an option from a dropdown/select element by its index.",
3421
- inputSchema: z4.object({
3422
- index: z4.number().describe("The index of the select element"),
3423
- value: z4.string().describe("The option text or value to select")
4327
+ inputSchema: z5.object({
4328
+ index: z5.number().describe("The index of the select element"),
4329
+ value: z5.string().describe("The option text or value to select")
3424
4330
  }),
3425
4331
  execute: async (args) => {
3426
4332
  await page.selectOption(args.index, args.value);
@@ -3430,12 +4336,12 @@ function createSelectTool(page) {
3430
4336
  }
3431
4337
 
3432
4338
  // src/agent/tools/wait.ts
3433
- import { z as z5 } from "zod";
4339
+ import { z as z6 } from "zod";
3434
4340
  function createWaitTool() {
3435
4341
  return {
3436
4342
  description: "Wait for a specified number of seconds before continuing.",
3437
- inputSchema: z5.object({
3438
- seconds: z5.number().min(0.1).max(30).describe("Seconds to wait")
4343
+ inputSchema: z6.object({
4344
+ seconds: z6.number().min(0.1).max(30).describe("Seconds to wait")
3439
4345
  }),
3440
4346
  execute: async (args) => {
3441
4347
  await new Promise((r) => setTimeout(r, args.seconds * 1e3));
@@ -3445,13 +4351,13 @@ function createWaitTool() {
3445
4351
  }
3446
4352
 
3447
4353
  // src/agent/tools/done.ts
3448
- import { z as z6 } from "zod";
4354
+ import { z as z7 } from "zod";
3449
4355
  function createDoneTool() {
3450
4356
  return {
3451
4357
  description: "Signal that the task is complete. Call this when you have finished the task or cannot proceed further.",
3452
- inputSchema: z6.object({
3453
- success: z6.boolean().describe("Whether the task was completed successfully"),
3454
- text: z6.string().describe("Summary of the result or explanation of failure")
4358
+ inputSchema: z7.object({
4359
+ success: z7.boolean().describe("Whether the task was completed successfully"),
4360
+ text: z7.string().describe("Summary of the result or explanation of failure")
3455
4361
  }),
3456
4362
  execute: async (args) => {
3457
4363
  return JSON.stringify({ done: true, success: args.success, text: args.text });
@@ -3460,13 +4366,13 @@ function createDoneTool() {
3460
4366
  }
3461
4367
 
3462
4368
  // src/agent/tools/ask-user.ts
3463
- import { z as z7 } from "zod";
4369
+ import { z as z8 } from "zod";
3464
4370
  import { createInterface } from "readline";
3465
4371
  function createAskUserTool() {
3466
4372
  return {
3467
4373
  description: "Ask the user a question when you need clarification or input to proceed.",
3468
- inputSchema: z7.object({
3469
- question: z7.string().describe("The question to ask the user")
4374
+ inputSchema: z8.object({
4375
+ question: z8.string().describe("The question to ask the user")
3470
4376
  }),
3471
4377
  execute: async (args) => {
3472
4378
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -3483,12 +4389,12 @@ function createAskUserTool() {
3483
4389
  }
3484
4390
 
3485
4391
  // src/agent/tools/execute-js.ts
3486
- import { z as z8 } from "zod";
4392
+ import { z as z9 } from "zod";
3487
4393
  function createExecuteJsTool(page) {
3488
4394
  return {
3489
4395
  description: "Execute JavaScript code on the current page. Returns the result.",
3490
- inputSchema: z8.object({
3491
- code: z8.string().describe("JavaScript code to execute on the page")
4396
+ inputSchema: z9.object({
4397
+ code: z9.string().describe("JavaScript code to execute on the page")
3492
4398
  }),
3493
4399
  execute: async (args) => {
3494
4400
  const result = await page.evaluate(args.code);
@@ -3512,7 +4418,7 @@ function createDefaultTools(page) {
3512
4418
  }
3513
4419
 
3514
4420
  // src/agent/macro-tool.ts
3515
- import { z as z9 } from "zod";
4421
+ import { z as z10 } from "zod";
3516
4422
 
3517
4423
  // src/agent/auto-fixer.ts
3518
4424
  function normalizeResponse(raw, toolName, availableActions, toolSchemas) {
@@ -3659,14 +4565,14 @@ function packMacroTool(tools) {
3659
4565
  for (const [name, tool] of Object.entries(tools)) {
3660
4566
  toolNames.push(name);
3661
4567
  actionSchemas.push(
3662
- z9.object({ [name]: tool.inputSchema }).describe(tool.description)
4568
+ z10.object({ [name]: tool.inputSchema }).describe(tool.description)
3663
4569
  );
3664
4570
  }
3665
- const actionSchema = actionSchemas.length === 1 ? actionSchemas[0] : z9.union(actionSchemas);
3666
- const macroSchema = z9.object({
3667
- evaluation_previous_goal: z9.string().optional().describe("Evaluate whether the previous goal was achieved"),
3668
- memory: z9.string().optional().describe("Important information to remember for future steps"),
3669
- next_goal: z9.string().optional().describe("The next immediate goal to achieve"),
4571
+ const actionSchema = actionSchemas.length === 1 ? actionSchemas[0] : z10.union(actionSchemas);
4572
+ const macroSchema = z10.object({
4573
+ evaluation_previous_goal: z10.string().optional().describe("Evaluate whether the previous goal was achieved"),
4574
+ memory: z10.string().optional().describe("Important information to remember for future steps"),
4575
+ next_goal: z10.string().optional().describe("The next immediate goal to achieve"),
3670
4576
  action: actionSchema.describe("The action to take")
3671
4577
  });
3672
4578
  return {
@@ -3755,7 +4661,7 @@ var AgentCore = class {
3755
4661
  const macroTool = packMacroTool(tools);
3756
4662
  let systemPrompt;
3757
4663
  try {
3758
- systemPrompt = readFileSync(join2(__dirname, "prompts", "system.md"), "utf-8");
4664
+ systemPrompt = readFileSync3(join4(__dirname, "prompts", "system.md"), "utf-8");
3759
4665
  } catch {
3760
4666
  systemPrompt = "You are an AI web agent that navigates web pages to complete tasks.";
3761
4667
  }
@@ -3944,128 +4850,104 @@ ${pageContent}
3944
4850
  return prompt;
3945
4851
  }
3946
4852
 
3947
- // src/config/index.ts
3948
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
3949
- import { join as join3 } from "path";
3950
- import { homedir } from "os";
3951
- import yaml2 from "js-yaml";
3952
-
3953
- // src/config/schema.ts
3954
- import { z as z10 } from "zod";
3955
- var LLM_PROVIDERS = {
3956
- openai: {
3957
- name: "OpenAI",
3958
- baseURL: "https://api.openai.com/v1",
3959
- defaultModel: "gpt-4o",
3960
- keyPrefix: "sk-",
3961
- keyEnvHint: "https://platform.openai.com/api-keys",
3962
- models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"]
3963
- },
3964
- anthropic: {
3965
- name: "Anthropic",
3966
- baseURL: "https://api.anthropic.com/v1",
3967
- defaultModel: "claude-sonnet-4-20250514",
3968
- keyPrefix: "sk-ant-",
3969
- keyEnvHint: "https://console.anthropic.com/settings/keys",
3970
- models: ["claude-opus-4-20250514", "claude-sonnet-4-20250514", "claude-haiku-4-5-20251001"]
3971
- },
3972
- gemini: {
3973
- name: "Google Gemini",
3974
- baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
3975
- defaultModel: "gemini-2.5-flash",
3976
- keyPrefix: "AI",
3977
- keyEnvHint: "https://aistudio.google.com/apikey",
3978
- models: ["gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.5-pro", "gemini-3-flash-preview"]
3979
- },
3980
- ollama: {
3981
- name: "Ollama (local, free)",
3982
- baseURL: "http://localhost:11434/v1",
3983
- defaultModel: "llama3.1",
3984
- keyPrefix: "",
3985
- keyEnvHint: "No API key needed \u2014 install from https://ollama.ai",
3986
- models: ["llama3.1", "llama3.2", "mistral", "codestral", "qwen2.5", "deepseek-r1"]
4853
+ // src/domain-guard.ts
4854
+ var DEFAULT_BLOCK_MESSAGE = "This domain is not allowed by the current configuration.";
4855
+ var DomainGuard = class {
4856
+ allow;
4857
+ block;
4858
+ message;
4859
+ matchSubs;
4860
+ constructor(config = {}) {
4861
+ this.allow = (config.allowDomains || []).map((d) => d.toLowerCase().replace(/^www\./, ""));
4862
+ this.block = (config.blockDomains || []).map((d) => d.toLowerCase().replace(/^www\./, ""));
4863
+ this.message = config.blockMessage || DEFAULT_BLOCK_MESSAGE;
4864
+ this.matchSubs = config.matchSubdomains ?? true;
3987
4865
  }
3988
- };
3989
- var configSchema = z10.object({
3990
- llm: z10.object({
3991
- provider: z10.enum(["openai", "anthropic", "gemini", "ollama"]).default("openai"),
3992
- baseURL: z10.string().default("https://api.openai.com/v1"),
3993
- model: z10.string().default("gpt-4o"),
3994
- apiKey: z10.string().default(""),
3995
- temperature: z10.number().min(0).max(2).default(0.1),
3996
- maxRetries: z10.number().int().min(0).default(3)
3997
- }).default({}),
3998
- browser: z10.object({
3999
- executablePath: z10.string().default(""),
4000
- headless: z10.boolean().default(true),
4001
- connectTimeout: z10.number().default(30),
4002
- commandTimeout: z10.number().default(60),
4003
- cdpEndpoint: z10.string().default("")
4004
- }).default({}),
4005
- agent: z10.object({
4006
- maxSteps: z10.number().int().default(40),
4007
- stepDelay: z10.number().default(0.4)
4008
- }).default({}),
4009
- output: z10.object({
4010
- defaultFormat: z10.enum(["table", "json", "yaml", "markdown", "csv"]).default("table"),
4011
- color: z10.boolean().default(true)
4012
- }).default({})
4013
- });
4014
-
4015
- // src/config/index.ts
4016
- var CONFIG_DIR = join3(homedir(), ".lobster");
4017
- var CONFIG_FILE = join3(CONFIG_DIR, "config.yaml");
4018
- function ensureConfigDir() {
4019
- if (!existsSync3(CONFIG_DIR)) {
4020
- mkdirSync2(CONFIG_DIR, { recursive: true });
4866
+ /**
4867
+ * Check if a URL is allowed. Returns true if allowed, throws if blocked.
4868
+ */
4869
+ check(url) {
4870
+ if (this.allow.length === 0 && this.block.length === 0) return true;
4871
+ const domain = this.extractDomain(url);
4872
+ if (!domain) return true;
4873
+ if (this.allow.length > 0) {
4874
+ if (!this.matches(domain, this.allow)) {
4875
+ throw new DomainBlockedError(domain, this.message);
4876
+ }
4877
+ return true;
4878
+ }
4879
+ if (this.block.length > 0) {
4880
+ if (this.matches(domain, this.block)) {
4881
+ throw new DomainBlockedError(domain, this.message);
4882
+ }
4883
+ return true;
4884
+ }
4885
+ return true;
4021
4886
  }
4022
- }
4023
- function loadConfig() {
4024
- ensureConfigDir();
4025
- let fileConfig = {};
4026
- if (existsSync3(CONFIG_FILE)) {
4027
- const raw = readFileSync2(CONFIG_FILE, "utf-8");
4028
- fileConfig = yaml2.load(raw) || {};
4887
+ /**
4888
+ * Check without throwing — returns { allowed, domain, message }.
4889
+ */
4890
+ test(url) {
4891
+ const domain = this.extractDomain(url);
4892
+ if (!domain) return { allowed: true, domain: "" };
4893
+ try {
4894
+ this.check(url);
4895
+ return { allowed: true, domain };
4896
+ } catch (err) {
4897
+ if (err instanceof DomainBlockedError) {
4898
+ return { allowed: false, domain, message: err.message };
4899
+ }
4900
+ return { allowed: true, domain };
4901
+ }
4029
4902
  }
4030
- const envOverrides = {};
4031
- if (process.env.LOBSTER_API_KEY) {
4032
- envOverrides.llm = { ...fileConfig.llm || {}, apiKey: process.env.LOBSTER_API_KEY };
4903
+ /**
4904
+ * Check if domain matches any pattern in the list.
4905
+ */
4906
+ matches(domain, patterns) {
4907
+ for (const pattern of patterns) {
4908
+ if (domain === pattern) return true;
4909
+ if (this.matchSubs && domain.endsWith("." + pattern)) return true;
4910
+ }
4911
+ return false;
4033
4912
  }
4034
- if (process.env.LOBSTER_MODEL) {
4035
- envOverrides.llm = { ...envOverrides.llm || fileConfig.llm || {}, model: process.env.LOBSTER_MODEL };
4913
+ /**
4914
+ * Extract domain from URL, stripping www. prefix.
4915
+ */
4916
+ extractDomain(url) {
4917
+ try {
4918
+ const parsed = new URL(url.startsWith("http") ? url : "https://" + url);
4919
+ return parsed.hostname.toLowerCase().replace(/^www\./, "");
4920
+ } catch {
4921
+ return "";
4922
+ }
4036
4923
  }
4037
- if (process.env.LOBSTER_BASE_URL) {
4038
- envOverrides.llm = { ...envOverrides.llm || fileConfig.llm || {}, baseURL: process.env.LOBSTER_BASE_URL };
4924
+ /**
4925
+ * Whether any restrictions are active.
4926
+ */
4927
+ get isRestricted() {
4928
+ return this.allow.length > 0 || this.block.length > 0;
4039
4929
  }
4040
- if (process.env.LOBSTER_CDP_ENDPOINT) {
4041
- envOverrides.browser = { ...fileConfig.browser || {}, cdpEndpoint: process.env.LOBSTER_CDP_ENDPOINT };
4930
+ /**
4931
+ * Get list of allowed domains (empty = all allowed).
4932
+ */
4933
+ get allowedDomains() {
4934
+ return [...this.allow];
4042
4935
  }
4043
- if (process.env.LOBSTER_BROWSER_PATH) {
4044
- envOverrides.browser = { ...envOverrides.browser || fileConfig.browser || {}, executablePath: process.env.LOBSTER_BROWSER_PATH };
4936
+ /**
4937
+ * Get list of blocked domains.
4938
+ */
4939
+ get blockedDomains() {
4940
+ return [...this.block];
4045
4941
  }
4046
- const merged = { ...fileConfig, ...envOverrides };
4047
- return configSchema.parse(merged);
4048
- }
4049
- function saveConfig(config) {
4050
- ensureConfigDir();
4051
- const existing = loadConfig();
4052
- const merged = deepMerge(existing, config);
4053
- writeFileSync2(CONFIG_FILE, yaml2.dump(merged, { indent: 2 }), "utf-8");
4054
- }
4055
- function getConfigDir() {
4056
- return CONFIG_DIR;
4057
- }
4058
- function deepMerge(target, source) {
4059
- const result = { ...target };
4060
- for (const key of Object.keys(source)) {
4061
- if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object" && !Array.isArray(target[key])) {
4062
- result[key] = deepMerge(target[key], source[key]);
4063
- } else {
4064
- result[key] = source[key];
4065
- }
4942
+ };
4943
+ var DomainBlockedError = class extends Error {
4944
+ domain;
4945
+ constructor(domain, message) {
4946
+ super(message);
4947
+ this.name = "DomainBlockedError";
4948
+ this.domain = domain;
4066
4949
  }
4067
- return result;
4068
- }
4950
+ };
4069
4951
 
4070
4952
  // src/output/table.ts
4071
4953
  import Table from "cli-table3";
@@ -4159,6 +5041,9 @@ function render(data, format, columns) {
4159
5041
  export {
4160
5042
  AgentCore,
4161
5043
  BrowserManager,
5044
+ COMPACT_SNAPSHOT_SCRIPT,
5045
+ DomainBlockedError,
5046
+ DomainGuard,
4162
5047
  FLAT_TREE_SCRIPT,
4163
5048
  FORM_STATE_SCRIPT,
4164
5049
  GET_INTERCEPTED_SCRIPT,
@@ -4170,13 +5055,18 @@ export {
4170
5055
  PuppeteerPage,
4171
5056
  SEMANTIC_TREE_SCRIPT,
4172
5057
  SNAPSHOT_SCRIPT,
5058
+ STEALTH_ARGS,
5059
+ STEALTH_SCRIPT,
4173
5060
  Strategy,
5061
+ buildCompactSnapshotScript,
4174
5062
  buildInterceptorScript,
4175
5063
  buildSnapshotScript,
4176
5064
  cascadeProbe,
4177
5065
  classifyIntent,
4178
5066
  cli,
4179
5067
  configSchema,
5068
+ createProfile,
5069
+ discoverChrome,
4180
5070
  executePipeline,
4181
5071
  exploreSite,
4182
5072
  extractLinks,
@@ -4190,17 +5080,24 @@ export {
4190
5080
  getAllAdapters,
4191
5081
  getAllSites,
4192
5082
  getConfigDir,
5083
+ getProfileDataDir,
4193
5084
  getStep,
4194
5085
  getStepNames,
4195
5086
  heuristicClassify,
5087
+ injectStealth,
5088
+ listProfiles,
4196
5089
  loadConfig,
4197
5090
  lobsterFetch,
4198
5091
  makeRoutingDecision,
4199
5092
  parseHtml,
4200
5093
  registerStep,
5094
+ removeProfile,
4201
5095
  render,
4202
5096
  renderTemplate,
5097
+ resetProfileCache,
5098
+ resolveAttachTarget,
4203
5099
  saveConfig,
5100
+ semanticFind,
4204
5101
  synthesizeAdapter
4205
5102
  };
4206
5103
  //# sourceMappingURL=lib.js.map