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/LICENSE +21 -0
- package/README.md +147 -269
- package/dist/browser/chrome-attach.js +102 -0
- package/dist/browser/chrome-attach.js.map +1 -0
- package/dist/browser/dom/compact-snapshot.js +162 -0
- package/dist/browser/dom/compact-snapshot.js.map +1 -0
- package/dist/browser/dom/index.js +160 -0
- package/dist/browser/dom/index.js.map +1 -1
- package/dist/browser/index.js +907 -70
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/manager.js +443 -11
- package/dist/browser/manager.js.map +1 -1
- package/dist/browser/page-adapter.js +370 -1
- package/dist/browser/page-adapter.js.map +1 -1
- package/dist/browser/profiles.js +238 -0
- package/dist/browser/profiles.js.map +1 -0
- package/dist/browser/semantic-find.js +152 -0
- package/dist/browser/semantic-find.js.map +1 -0
- package/dist/browser/stealth.js +187 -0
- package/dist/browser/stealth.js.map +1 -0
- package/dist/config/index.js +8 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.js +8 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/domain-guard.js +103 -0
- package/dist/domain-guard.js.map +1 -0
- package/dist/index.js +851 -48
- package/dist/index.js.map +1 -1
- package/dist/lib.js +1141 -244
- package/dist/lib.js.map +1 -1
- package/dist/router/index.js +862 -61
- package/dist/router/index.js.map +1 -1
- package/package.json +2 -1
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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(
|
|
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
|
|
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 (!
|
|
2925
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3885
|
+
writeFileSync3(join3(dir, "auth.json"), JSON.stringify(authSummary, null, 2));
|
|
2980
3886
|
if (result.stores && result.stores.length > 0) {
|
|
2981
|
-
|
|
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 ||
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
3375
|
-
index:
|
|
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
|
|
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:
|
|
3390
|
-
index:
|
|
3391
|
-
text:
|
|
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
|
|
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:
|
|
3406
|
-
direction:
|
|
3407
|
-
amount:
|
|
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
|
|
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:
|
|
3422
|
-
index:
|
|
3423
|
-
value:
|
|
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
|
|
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:
|
|
3438
|
-
seconds:
|
|
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
|
|
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:
|
|
3453
|
-
success:
|
|
3454
|
-
text:
|
|
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
|
|
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:
|
|
3469
|
-
question:
|
|
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
|
|
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:
|
|
3491
|
-
code:
|
|
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
|
|
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
|
-
|
|
4568
|
+
z10.object({ [name]: tool.inputSchema }).describe(tool.description)
|
|
3663
4569
|
);
|
|
3664
4570
|
}
|
|
3665
|
-
const actionSchema = actionSchemas.length === 1 ? actionSchemas[0] :
|
|
3666
|
-
const macroSchema =
|
|
3667
|
-
evaluation_previous_goal:
|
|
3668
|
-
memory:
|
|
3669
|
-
next_goal:
|
|
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 =
|
|
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/
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
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
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
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
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
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
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
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
|
-
|
|
4035
|
-
|
|
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
|
-
|
|
4038
|
-
|
|
4924
|
+
/**
|
|
4925
|
+
* Whether any restrictions are active.
|
|
4926
|
+
*/
|
|
4927
|
+
get isRestricted() {
|
|
4928
|
+
return this.allow.length > 0 || this.block.length > 0;
|
|
4039
4929
|
}
|
|
4040
|
-
|
|
4041
|
-
|
|
4930
|
+
/**
|
|
4931
|
+
* Get list of allowed domains (empty = all allowed).
|
|
4932
|
+
*/
|
|
4933
|
+
get allowedDomains() {
|
|
4934
|
+
return [...this.allow];
|
|
4042
4935
|
}
|
|
4043
|
-
|
|
4044
|
-
|
|
4936
|
+
/**
|
|
4937
|
+
* Get list of blocked domains.
|
|
4938
|
+
*/
|
|
4939
|
+
get blockedDomains() {
|
|
4940
|
+
return [...this.block];
|
|
4045
4941
|
}
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
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
|
-
|
|
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
|