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.
@@ -0,0 +1,238 @@
1
+ // src/browser/profiles.ts
2
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync, rmSync, statSync } from "fs";
3
+ import { join as join2 } from "path";
4
+
5
+ // src/config/index.ts
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
7
+ import { join } from "path";
8
+ import { homedir } from "os";
9
+ import yaml from "js-yaml";
10
+
11
+ // src/config/schema.ts
12
+ import { z } from "zod";
13
+ var configSchema = z.object({
14
+ llm: z.object({
15
+ provider: z.enum(["openai", "anthropic", "gemini", "ollama"]).default("openai"),
16
+ baseURL: z.string().default("https://api.openai.com/v1"),
17
+ model: z.string().default("gpt-4o"),
18
+ apiKey: z.string().default(""),
19
+ temperature: z.number().min(0).max(2).default(0.1),
20
+ maxRetries: z.number().int().min(0).default(3)
21
+ }).default({}),
22
+ browser: z.object({
23
+ executablePath: z.string().default(""),
24
+ headless: z.boolean().default(true),
25
+ connectTimeout: z.number().default(30),
26
+ commandTimeout: z.number().default(60),
27
+ cdpEndpoint: z.string().default(""),
28
+ profile: z.string().default(""),
29
+ stealth: z.boolean().default(false)
30
+ }).default({}),
31
+ agent: z.object({
32
+ maxSteps: z.number().int().default(40),
33
+ stepDelay: z.number().default(0.4)
34
+ }).default({}),
35
+ domains: z.object({
36
+ allow: z.array(z.string()).default([]),
37
+ block: z.array(z.string()).default([]),
38
+ blockMessage: z.string().default("")
39
+ }).default({}),
40
+ output: z.object({
41
+ defaultFormat: z.enum(["table", "json", "yaml", "markdown", "csv"]).default("table"),
42
+ color: z.boolean().default(true)
43
+ }).default({})
44
+ });
45
+
46
+ // src/config/index.ts
47
+ var CONFIG_DIR = join(homedir(), ".lobster");
48
+ var CONFIG_FILE = join(CONFIG_DIR, "config.yaml");
49
+ function getConfigDir() {
50
+ return CONFIG_DIR;
51
+ }
52
+
53
+ // src/utils/logger.ts
54
+ import chalk from "chalk";
55
+ var log = {
56
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
57
+ success: (msg) => console.log(chalk.green("\u2713"), msg),
58
+ warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
59
+ error: (msg) => console.error(chalk.red("\u2717"), msg),
60
+ debug: (msg) => {
61
+ if (process.env.LOBSTER_DEBUG) console.log(chalk.gray("\u22EF"), msg);
62
+ },
63
+ step: (n, msg) => console.log(chalk.cyan(`[${n}]`), msg),
64
+ dim: (msg) => console.log(chalk.dim(msg))
65
+ };
66
+
67
+ // src/browser/profiles.ts
68
+ var PROFILES_DIR = () => join2(getConfigDir(), "profiles");
69
+ var META_FILE = ".lobster-meta.json";
70
+ var VALID_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
71
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
72
+ "default",
73
+ "system",
74
+ "con",
75
+ "prn",
76
+ "aux",
77
+ "nul",
78
+ "com1",
79
+ "com2",
80
+ "com3",
81
+ "com4",
82
+ "com5",
83
+ "com6",
84
+ "com7",
85
+ "com8",
86
+ "com9",
87
+ "lpt1",
88
+ "lpt2",
89
+ "lpt3",
90
+ "lpt4",
91
+ "lpt5",
92
+ "lpt6",
93
+ "lpt7",
94
+ "lpt8",
95
+ "lpt9"
96
+ ]);
97
+ var CACHE_DIRS = [
98
+ "Cache",
99
+ "Code Cache",
100
+ "GPUCache",
101
+ "GrShaderCache",
102
+ "ShaderCache",
103
+ "Service Worker",
104
+ "Sessions",
105
+ "Session Storage",
106
+ "blob_storage"
107
+ ];
108
+ function ensureProfilesDir() {
109
+ const dir = PROFILES_DIR();
110
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
111
+ }
112
+ function validateName(name) {
113
+ if (!VALID_NAME.test(name)) {
114
+ throw new Error(`Invalid profile name "${name}". Use only letters, numbers, hyphens, underscores (max 64 chars).`);
115
+ }
116
+ if (RESERVED_NAMES.has(name.toLowerCase())) {
117
+ throw new Error(`"${name}" is a reserved name. Choose a different profile name.`);
118
+ }
119
+ }
120
+ function getProfileDir(name) {
121
+ return join2(PROFILES_DIR(), name);
122
+ }
123
+ function readMeta(profileDir) {
124
+ const metaPath = join2(profileDir, META_FILE);
125
+ if (!existsSync2(metaPath)) return null;
126
+ try {
127
+ return JSON.parse(readFileSync2(metaPath, "utf-8"));
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ function writeMeta(profileDir, meta) {
133
+ writeFileSync2(join2(profileDir, META_FILE), JSON.stringify(meta, null, 2));
134
+ }
135
+ function getDirSizeMB(dirPath) {
136
+ let total = 0;
137
+ try {
138
+ const entries = readdirSync(dirPath, { withFileTypes: true });
139
+ for (const entry of entries) {
140
+ const fullPath = join2(dirPath, entry.name);
141
+ if (entry.isFile()) {
142
+ total += statSync(fullPath).size;
143
+ } else if (entry.isDirectory() && entry.name !== ".lobster-meta.json") {
144
+ total += getDirSizeMB(fullPath) * 1024 * 1024;
145
+ }
146
+ }
147
+ } catch {
148
+ }
149
+ return Math.round(total / (1024 * 1024) * 10) / 10;
150
+ }
151
+ function createProfile(name) {
152
+ validateName(name);
153
+ ensureProfilesDir();
154
+ const dir = getProfileDir(name);
155
+ if (existsSync2(dir)) {
156
+ throw new Error(`Profile "${name}" already exists.`);
157
+ }
158
+ mkdirSync2(dir, { recursive: true });
159
+ const meta = {
160
+ name,
161
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
162
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString()
163
+ };
164
+ writeMeta(dir, meta);
165
+ log.success(`Profile "${name}" created at ${dir}`);
166
+ return meta;
167
+ }
168
+ function listProfiles() {
169
+ ensureProfilesDir();
170
+ const dir = PROFILES_DIR();
171
+ const profiles = [];
172
+ try {
173
+ const entries = readdirSync(dir, { withFileTypes: true });
174
+ for (const entry of entries) {
175
+ if (!entry.isDirectory()) continue;
176
+ const profileDir = join2(dir, entry.name);
177
+ const meta = readMeta(profileDir);
178
+ if (meta) {
179
+ meta.sizeMB = getDirSizeMB(profileDir);
180
+ profiles.push(meta);
181
+ } else {
182
+ profiles.push({
183
+ name: entry.name,
184
+ createdAt: "unknown",
185
+ lastUsed: "unknown",
186
+ sizeMB: getDirSizeMB(profileDir)
187
+ });
188
+ }
189
+ }
190
+ } catch {
191
+ }
192
+ return profiles.sort((a, b) => a.name.localeCompare(b.name));
193
+ }
194
+ function removeProfile(name) {
195
+ const dir = getProfileDir(name);
196
+ if (!existsSync2(dir)) {
197
+ throw new Error(`Profile "${name}" does not exist.`);
198
+ }
199
+ rmSync(dir, { recursive: true, force: true });
200
+ log.success(`Profile "${name}" deleted.`);
201
+ }
202
+ function getProfileDataDir(name) {
203
+ validateName(name);
204
+ const dir = getProfileDir(name);
205
+ if (!existsSync2(dir)) {
206
+ createProfile(name);
207
+ } else {
208
+ const meta = readMeta(dir) || { name, createdAt: "unknown", lastUsed: "" };
209
+ meta.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
210
+ writeMeta(dir, meta);
211
+ }
212
+ return dir;
213
+ }
214
+ function resetProfileCache(name) {
215
+ const dir = getProfileDir(name);
216
+ if (!existsSync2(dir)) {
217
+ throw new Error(`Profile "${name}" does not exist.`);
218
+ }
219
+ let cleaned = 0;
220
+ for (const cacheDir of CACHE_DIRS) {
221
+ for (const base of [dir, join2(dir, "Default")]) {
222
+ const target = join2(base, cacheDir);
223
+ if (existsSync2(target)) {
224
+ rmSync(target, { recursive: true, force: true });
225
+ cleaned++;
226
+ }
227
+ }
228
+ }
229
+ log.success(`Profile "${name}" cache reset (${cleaned} directories cleaned).`);
230
+ }
231
+ export {
232
+ createProfile,
233
+ getProfileDataDir,
234
+ listProfiles,
235
+ removeProfile,
236
+ resetProfileCache
237
+ };
238
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/browser/profiles.ts","../../src/config/index.ts","../../src/config/schema.ts","../../src/utils/logger.ts"],"sourcesContent":["/**\n * Persistent Profiles — store Chrome user data dirs so cookies,\n * auth, and extensions survive across sessions.\n *\n * Inspired by PinchTab's profile management, built from scratch.\n *\n * Storage: ~/.lobster/profiles/<name>/\n * Metadata: ~/.lobster/profiles/<name>/.lobster-meta.json\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { getConfigDir } from '../config/index.js';\nimport { log } from '../utils/logger.js';\n\nexport interface ProfileMeta {\n name: string;\n createdAt: string;\n lastUsed: string;\n sizeMB?: number;\n}\n\nconst PROFILES_DIR = () => join(getConfigDir(), 'profiles');\nconst META_FILE = '.lobster-meta.json';\n\n// Name validation: safe across Windows/Mac/Linux, no path traversal\nconst VALID_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\nconst RESERVED_NAMES = new Set([\n 'default', 'system', 'con', 'prn', 'aux', 'nul',\n 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',\n 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',\n]);\n\n// Directories to delete on cache reset (keep cookies, extensions, local storage)\nconst CACHE_DIRS = [\n 'Cache', 'Code Cache', 'GPUCache', 'GrShaderCache', 'ShaderCache',\n 'Service Worker', 'Sessions', 'Session Storage', 'blob_storage',\n];\n\nfunction ensureProfilesDir(): void {\n const dir = PROFILES_DIR();\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n}\n\nfunction validateName(name: string): void {\n if (!VALID_NAME.test(name)) {\n throw new Error(`Invalid profile name \"${name}\". Use only letters, numbers, hyphens, underscores (max 64 chars).`);\n }\n if (RESERVED_NAMES.has(name.toLowerCase())) {\n throw new Error(`\"${name}\" is a reserved name. Choose a different profile name.`);\n }\n}\n\nfunction getProfileDir(name: string): string {\n return join(PROFILES_DIR(), name);\n}\n\nfunction readMeta(profileDir: string): ProfileMeta | null {\n const metaPath = join(profileDir, META_FILE);\n if (!existsSync(metaPath)) return null;\n try {\n return JSON.parse(readFileSync(metaPath, 'utf-8'));\n } catch {\n return null;\n }\n}\n\nfunction writeMeta(profileDir: string, meta: ProfileMeta): void {\n writeFileSync(join(profileDir, META_FILE), JSON.stringify(meta, null, 2));\n}\n\nfunction getDirSizeMB(dirPath: string): number {\n let total = 0;\n try {\n const entries = readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = join(dirPath, entry.name);\n if (entry.isFile()) {\n total += statSync(fullPath).size;\n } else if (entry.isDirectory() && entry.name !== '.lobster-meta.json') {\n total += getDirSizeMB(fullPath) * 1024 * 1024; // recursive returns MB\n }\n }\n } catch {}\n return Math.round((total / (1024 * 1024)) * 10) / 10;\n}\n\n/**\n * Create a new profile.\n */\nexport function createProfile(name: string): ProfileMeta {\n validateName(name);\n ensureProfilesDir();\n\n const dir = getProfileDir(name);\n if (existsSync(dir)) {\n throw new Error(`Profile \"${name}\" already exists.`);\n }\n\n mkdirSync(dir, { recursive: true });\n\n const meta: ProfileMeta = {\n name,\n createdAt: new Date().toISOString(),\n lastUsed: new Date().toISOString(),\n };\n\n writeMeta(dir, meta);\n log.success(`Profile \"${name}\" created at ${dir}`);\n return meta;\n}\n\n/**\n * List all profiles.\n */\nexport function listProfiles(): ProfileMeta[] {\n ensureProfilesDir();\n const dir = PROFILES_DIR();\n const profiles: ProfileMeta[] = [];\n\n try {\n const entries = readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const profileDir = join(dir, entry.name);\n const meta = readMeta(profileDir);\n if (meta) {\n meta.sizeMB = getDirSizeMB(profileDir);\n profiles.push(meta);\n } else {\n // Directory exists but no meta — create meta\n profiles.push({\n name: entry.name,\n createdAt: 'unknown',\n lastUsed: 'unknown',\n sizeMB: getDirSizeMB(profileDir),\n });\n }\n }\n } catch {}\n\n return profiles.sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/**\n * Remove a profile and all its data.\n */\nexport function removeProfile(name: string): void {\n const dir = getProfileDir(name);\n if (!existsSync(dir)) {\n throw new Error(`Profile \"${name}\" does not exist.`);\n }\n\n rmSync(dir, { recursive: true, force: true });\n log.success(`Profile \"${name}\" deleted.`);\n}\n\n/**\n * Get the Chrome user data directory for a profile.\n * Updates lastUsed timestamp.\n */\nexport function getProfileDataDir(name: string): string {\n validateName(name);\n const dir = getProfileDir(name);\n\n if (!existsSync(dir)) {\n // Auto-create if doesn't exist\n createProfile(name);\n } else {\n // Update lastUsed\n const meta = readMeta(dir) || { name, createdAt: 'unknown', lastUsed: '' };\n meta.lastUsed = new Date().toISOString();\n writeMeta(dir, meta);\n }\n\n return dir;\n}\n\n/**\n * Reset cache directories but keep cookies, extensions, and local storage.\n */\nexport function resetProfileCache(name: string): void {\n const dir = getProfileDir(name);\n if (!existsSync(dir)) {\n throw new Error(`Profile \"${name}\" does not exist.`);\n }\n\n let cleaned = 0;\n for (const cacheDir of CACHE_DIRS) {\n // Check both root and Default/ subdirectory\n for (const base of [dir, join(dir, 'Default')]) {\n const target = join(base, cacheDir);\n if (existsSync(target)) {\n rmSync(target, { recursive: true, force: true });\n cleaned++;\n }\n }\n }\n\n log.success(`Profile \"${name}\" cache reset (${cleaned} directories cleaned).`);\n}\n","import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport yaml from 'js-yaml';\nimport { configSchema, type LobsterConfig } from './schema.js';\nimport { DEFAULT_CONFIG } from './defaults.js';\n\nexport type { LobsterConfig };\n\nconst CONFIG_DIR = join(homedir(), '.lobster');\nconst CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');\n\nfunction ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n}\n\nexport function loadConfig(): LobsterConfig {\n ensureConfigDir();\n\n let fileConfig: Record<string, unknown> = {};\n if (existsSync(CONFIG_FILE)) {\n const raw = readFileSync(CONFIG_FILE, 'utf-8');\n fileConfig = (yaml.load(raw) as Record<string, unknown>) || {};\n }\n\n // Env var overrides\n const envOverrides: Record<string, unknown> = {};\n if (process.env.LOBSTER_API_KEY) {\n envOverrides.llm = { ...(fileConfig.llm as Record<string, unknown> || {}), apiKey: process.env.LOBSTER_API_KEY };\n }\n if (process.env.LOBSTER_MODEL) {\n envOverrides.llm = { ...(envOverrides.llm as Record<string, unknown> || fileConfig.llm as Record<string, unknown> || {}), model: process.env.LOBSTER_MODEL };\n }\n if (process.env.LOBSTER_BASE_URL) {\n envOverrides.llm = { ...(envOverrides.llm as Record<string, unknown> || fileConfig.llm as Record<string, unknown> || {}), baseURL: process.env.LOBSTER_BASE_URL };\n }\n if (process.env.LOBSTER_CDP_ENDPOINT) {\n envOverrides.browser = { ...(fileConfig.browser as Record<string, unknown> || {}), cdpEndpoint: process.env.LOBSTER_CDP_ENDPOINT };\n }\n if (process.env.LOBSTER_BROWSER_PATH) {\n envOverrides.browser = { ...(envOverrides.browser as Record<string, unknown> || fileConfig.browser as Record<string, unknown> || {}), executablePath: process.env.LOBSTER_BROWSER_PATH };\n }\n\n const merged = { ...fileConfig, ...envOverrides };\n return configSchema.parse(merged);\n}\n\nexport function saveConfig(config: Partial<LobsterConfig>): void {\n ensureConfigDir();\n const existing = loadConfig();\n const merged = deepMerge(existing, config);\n writeFileSync(CONFIG_FILE, yaml.dump(merged, { indent: 2 }), 'utf-8');\n}\n\nexport function setConfigValue(keyPath: string, value: string): void {\n const parts = keyPath.split('.');\n const obj: Record<string, unknown> = {};\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n current[parts[i]] = {};\n current = current[parts[i]] as Record<string, unknown>;\n }\n // Try to parse as number or boolean\n let parsed: unknown = value;\n if (value === 'true') parsed = true;\n else if (value === 'false') parsed = false;\n else if (!isNaN(Number(value)) && value !== '') parsed = Number(value);\n\n current[parts[parts.length - 1]] = parsed;\n saveConfig(obj as Partial<LobsterConfig>);\n}\n\nexport function getConfigDir(): string {\n return CONFIG_DIR;\n}\n\nfunction deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&\n target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {\n result[key] = deepMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);\n } else {\n result[key] = source[key];\n }\n }\n return result;\n}\n","import { z } from 'zod';\n\nexport const LLM_PROVIDERS = {\n openai: {\n name: 'OpenAI',\n baseURL: 'https://api.openai.com/v1',\n defaultModel: 'gpt-4o',\n keyPrefix: 'sk-',\n keyEnvHint: 'https://platform.openai.com/api-keys',\n models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1', 'o1-mini', 'o3-mini'],\n },\n anthropic: {\n name: 'Anthropic',\n baseURL: 'https://api.anthropic.com/v1',\n defaultModel: 'claude-sonnet-4-20250514',\n keyPrefix: 'sk-ant-',\n keyEnvHint: 'https://console.anthropic.com/settings/keys',\n models: ['claude-opus-4-20250514', 'claude-sonnet-4-20250514', 'claude-haiku-4-5-20251001'],\n },\n gemini: {\n name: 'Google Gemini',\n baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai',\n defaultModel: 'gemini-2.5-flash',\n keyPrefix: 'AI',\n keyEnvHint: 'https://aistudio.google.com/apikey',\n models: ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro', 'gemini-3-flash-preview'],\n },\n ollama: {\n name: 'Ollama (local, free)',\n baseURL: 'http://localhost:11434/v1',\n defaultModel: 'llama3.1',\n keyPrefix: '',\n keyEnvHint: 'No API key needed — install from https://ollama.ai',\n models: ['llama3.1', 'llama3.2', 'mistral', 'codestral', 'qwen2.5', 'deepseek-r1'],\n },\n} as const;\n\nexport type LLMProvider = keyof typeof LLM_PROVIDERS;\n\nexport const configSchema = z.object({\n llm: z.object({\n provider: z.enum(['openai', 'anthropic', 'gemini', 'ollama']).default('openai'),\n baseURL: z.string().default('https://api.openai.com/v1'),\n model: z.string().default('gpt-4o'),\n apiKey: z.string().default(''),\n temperature: z.number().min(0).max(2).default(0.1),\n maxRetries: z.number().int().min(0).default(3),\n }).default({}),\n browser: z.object({\n executablePath: z.string().default(''),\n headless: z.boolean().default(true),\n connectTimeout: z.number().default(30),\n commandTimeout: z.number().default(60),\n cdpEndpoint: z.string().default(''),\n profile: z.string().default(''),\n stealth: z.boolean().default(false),\n }).default({}),\n agent: z.object({\n maxSteps: z.number().int().default(40),\n stepDelay: z.number().default(0.4),\n }).default({}),\n domains: z.object({\n allow: z.array(z.string()).default([]),\n block: z.array(z.string()).default([]),\n blockMessage: z.string().default(''),\n }).default({}),\n output: z.object({\n defaultFormat: z.enum(['table', 'json', 'yaml', 'markdown', 'csv']).default('table'),\n color: z.boolean().default(true),\n }).default({}),\n});\n\nexport type LobsterConfig = z.infer<typeof configSchema>;\n","import chalk from 'chalk';\n\nexport const log = {\n info: (msg: string) => console.log(chalk.blue('ℹ'), msg),\n success: (msg: string) => console.log(chalk.green('✓'), msg),\n warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),\n error: (msg: string) => console.error(chalk.red('✗'), msg),\n debug: (msg: string) => {\n if (process.env.LOBSTER_DEBUG) console.log(chalk.gray('⋯'), msg);\n },\n step: (n: number, msg: string) => console.log(chalk.cyan(`[${n}]`), msg),\n dim: (msg: string) => console.log(chalk.dim(msg)),\n};\n"],"mappings":";AAUA,SAAS,cAAAA,aAAY,aAAAC,YAAW,gBAAAC,eAAc,iBAAAC,gBAAe,aAAa,QAAQ,gBAAgB;AAClG,SAAS,QAAAC,aAAY;;;ACXrB,SAAS,cAAc,eAAe,WAAW,kBAAkB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,OAAO,UAAU;;;ACHjB,SAAS,SAAS;AAuCX,IAAM,eAAe,EAAE,OAAO;AAAA,EACnC,KAAK,EAAE,OAAO;AAAA,IACZ,UAAU,EAAE,KAAK,CAAC,UAAU,aAAa,UAAU,QAAQ,CAAC,EAAE,QAAQ,QAAQ;AAAA,IAC9E,SAAS,EAAE,OAAO,EAAE,QAAQ,2BAA2B;AAAA,IACvD,OAAO,EAAE,OAAO,EAAE,QAAQ,QAAQ;AAAA,IAClC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE;AAAA,IAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG;AAAA,IACjD,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EAC/C,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,EACb,SAAS,EAAE,OAAO;AAAA,IAChB,gBAAgB,EAAE,OAAO,EAAE,QAAQ,EAAE;AAAA,IACrC,UAAU,EAAE,QAAQ,EAAE,QAAQ,IAAI;AAAA,IAClC,gBAAgB,EAAE,OAAO,EAAE,QAAQ,EAAE;AAAA,IACrC,gBAAgB,EAAE,OAAO,EAAE,QAAQ,EAAE;AAAA,IACrC,aAAa,EAAE,OAAO,EAAE,QAAQ,EAAE;AAAA,IAClC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE;AAAA,IAC9B,SAAS,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACpC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,EACb,OAAO,EAAE,OAAO;AAAA,IACd,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE;AAAA,IACrC,WAAW,EAAE,OAAO,EAAE,QAAQ,GAAG;AAAA,EACnC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,EACb,SAAS,EAAE,OAAO;AAAA,IAChB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,IACrC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,IACrC,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE;AAAA,EACrC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,EACb,QAAQ,EAAE,OAAO;AAAA,IACf,eAAe,EAAE,KAAK,CAAC,SAAS,QAAQ,QAAQ,YAAY,KAAK,CAAC,EAAE,QAAQ,OAAO;AAAA,IACnF,OAAO,EAAE,QAAQ,EAAE,QAAQ,IAAI;AAAA,EACjC,CAAC,EAAE,QAAQ,CAAC,CAAC;AACf,CAAC;;;AD7DD,IAAM,aAAa,KAAK,QAAQ,GAAG,UAAU;AAC7C,IAAM,cAAc,KAAK,YAAY,aAAa;AAgE3C,SAAS,eAAuB;AACrC,SAAO;AACT;;;AE5EA,OAAO,WAAW;AAEX,IAAM,MAAM;AAAA,EACjB,MAAM,CAAC,QAAgB,QAAQ,IAAI,MAAM,KAAK,QAAG,GAAG,GAAG;AAAA,EACvD,SAAS,CAAC,QAAgB,QAAQ,IAAI,MAAM,MAAM,QAAG,GAAG,GAAG;AAAA,EAC3D,MAAM,CAAC,QAAgB,QAAQ,IAAI,MAAM,OAAO,QAAG,GAAG,GAAG;AAAA,EACzD,OAAO,CAAC,QAAgB,QAAQ,MAAM,MAAM,IAAI,QAAG,GAAG,GAAG;AAAA,EACzD,OAAO,CAAC,QAAgB;AACtB,QAAI,QAAQ,IAAI,cAAe,SAAQ,IAAI,MAAM,KAAK,QAAG,GAAG,GAAG;AAAA,EACjE;AAAA,EACA,MAAM,CAAC,GAAW,QAAgB,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,GAAG,GAAG,GAAG;AAAA,EACvE,KAAK,CAAC,QAAgB,QAAQ,IAAI,MAAM,IAAI,GAAG,CAAC;AAClD;;;AHUA,IAAM,eAAe,MAAMC,MAAK,aAAa,GAAG,UAAU;AAC1D,IAAM,YAAY;AAGlB,IAAM,aAAa;AACnB,IAAM,iBAAiB,oBAAI,IAAI;AAAA,EAC7B;AAAA,EAAW;AAAA,EAAU;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAC1C;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAChE;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAClE,CAAC;AAGD,IAAM,aAAa;AAAA,EACjB;AAAA,EAAS;AAAA,EAAc;AAAA,EAAY;AAAA,EAAiB;AAAA,EACpD;AAAA,EAAkB;AAAA,EAAY;AAAA,EAAmB;AACnD;AAEA,SAAS,oBAA0B;AACjC,QAAM,MAAM,aAAa;AACzB,MAAI,CAACC,YAAW,GAAG,EAAG,CAAAC,WAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAC1D;AAEA,SAAS,aAAa,MAAoB;AACxC,MAAI,CAAC,WAAW,KAAK,IAAI,GAAG;AAC1B,UAAM,IAAI,MAAM,yBAAyB,IAAI,oEAAoE;AAAA,EACnH;AACA,MAAI,eAAe,IAAI,KAAK,YAAY,CAAC,GAAG;AAC1C,UAAM,IAAI,MAAM,IAAI,IAAI,wDAAwD;AAAA,EAClF;AACF;AAEA,SAAS,cAAc,MAAsB;AAC3C,SAAOF,MAAK,aAAa,GAAG,IAAI;AAClC;AAEA,SAAS,SAAS,YAAwC;AACxD,QAAM,WAAWA,MAAK,YAAY,SAAS;AAC3C,MAAI,CAACC,YAAW,QAAQ,EAAG,QAAO;AAClC,MAAI;AACF,WAAO,KAAK,MAAME,cAAa,UAAU,OAAO,CAAC;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,UAAU,YAAoB,MAAyB;AAC9D,EAAAC,eAAcJ,MAAK,YAAY,SAAS,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAC1E;AAEA,SAAS,aAAa,SAAyB;AAC7C,MAAI,QAAQ;AACZ,MAAI;AACF,UAAM,UAAU,YAAY,SAAS,EAAE,eAAe,KAAK,CAAC;AAC5D,eAAW,SAAS,SAAS;AAC3B,YAAM,WAAWA,MAAK,SAAS,MAAM,IAAI;AACzC,UAAI,MAAM,OAAO,GAAG;AAClB,iBAAS,SAAS,QAAQ,EAAE;AAAA,MAC9B,WAAW,MAAM,YAAY,KAAK,MAAM,SAAS,sBAAsB;AACrE,iBAAS,aAAa,QAAQ,IAAI,OAAO;AAAA,MAC3C;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAAC;AACT,SAAO,KAAK,MAAO,SAAS,OAAO,QAAS,EAAE,IAAI;AACpD;AAKO,SAAS,cAAc,MAA2B;AACvD,eAAa,IAAI;AACjB,oBAAkB;AAElB,QAAM,MAAM,cAAc,IAAI;AAC9B,MAAIC,YAAW,GAAG,GAAG;AACnB,UAAM,IAAI,MAAM,YAAY,IAAI,mBAAmB;AAAA,EACrD;AAEA,EAAAC,WAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAElC,QAAM,OAAoB;AAAA,IACxB;AAAA,IACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,WAAU,oBAAI,KAAK,GAAE,YAAY;AAAA,EACnC;AAEA,YAAU,KAAK,IAAI;AACnB,MAAI,QAAQ,YAAY,IAAI,gBAAgB,GAAG,EAAE;AACjD,SAAO;AACT;AAKO,SAAS,eAA8B;AAC5C,oBAAkB;AAClB,QAAM,MAAM,aAAa;AACzB,QAAM,WAA0B,CAAC;AAEjC,MAAI;AACF,UAAM,UAAU,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC;AACxD,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,YAAY,EAAG;AAC1B,YAAM,aAAaF,MAAK,KAAK,MAAM,IAAI;AACvC,YAAM,OAAO,SAAS,UAAU;AAChC,UAAI,MAAM;AACR,aAAK,SAAS,aAAa,UAAU;AACrC,iBAAS,KAAK,IAAI;AAAA,MACpB,OAAO;AAEL,iBAAS,KAAK;AAAA,UACZ,MAAM,MAAM;AAAA,UACZ,WAAW;AAAA,UACX,UAAU;AAAA,UACV,QAAQ,aAAa,UAAU;AAAA,QACjC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAAC;AAET,SAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAC7D;AAKO,SAAS,cAAc,MAAoB;AAChD,QAAM,MAAM,cAAc,IAAI;AAC9B,MAAI,CAACC,YAAW,GAAG,GAAG;AACpB,UAAM,IAAI,MAAM,YAAY,IAAI,mBAAmB;AAAA,EACrD;AAEA,SAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC5C,MAAI,QAAQ,YAAY,IAAI,YAAY;AAC1C;AAMO,SAAS,kBAAkB,MAAsB;AACtD,eAAa,IAAI;AACjB,QAAM,MAAM,cAAc,IAAI;AAE9B,MAAI,CAACA,YAAW,GAAG,GAAG;AAEpB,kBAAc,IAAI;AAAA,EACpB,OAAO;AAEL,UAAM,OAAO,SAAS,GAAG,KAAK,EAAE,MAAM,WAAW,WAAW,UAAU,GAAG;AACzE,SAAK,YAAW,oBAAI,KAAK,GAAE,YAAY;AACvC,cAAU,KAAK,IAAI;AAAA,EACrB;AAEA,SAAO;AACT;AAKO,SAAS,kBAAkB,MAAoB;AACpD,QAAM,MAAM,cAAc,IAAI;AAC9B,MAAI,CAACA,YAAW,GAAG,GAAG;AACpB,UAAM,IAAI,MAAM,YAAY,IAAI,mBAAmB;AAAA,EACrD;AAEA,MAAI,UAAU;AACd,aAAW,YAAY,YAAY;AAEjC,eAAW,QAAQ,CAAC,KAAKD,MAAK,KAAK,SAAS,CAAC,GAAG;AAC9C,YAAM,SAASA,MAAK,MAAM,QAAQ;AAClC,UAAIC,YAAW,MAAM,GAAG;AACtB,eAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC/C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,YAAY,IAAI,kBAAkB,OAAO,wBAAwB;AAC/E;","names":["existsSync","mkdirSync","readFileSync","writeFileSync","join","join","existsSync","mkdirSync","readFileSync","writeFileSync"]}
@@ -0,0 +1,152 @@
1
+ // src/browser/semantic-find.ts
2
+ var SYNONYMS = {
3
+ btn: ["button"],
4
+ button: ["btn", "submit", "click"],
5
+ submit: ["go", "send", "ok", "confirm", "done", "button"],
6
+ search: ["find", "lookup", "query", "filter"],
7
+ login: ["signin", "sign-in", "log-in", "authenticate"],
8
+ signup: ["register", "create-account", "sign-up", "join"],
9
+ logout: ["signout", "sign-out", "log-out"],
10
+ close: ["dismiss", "x", "cancel", "exit"],
11
+ menu: ["nav", "navigation", "hamburger", "sidebar"],
12
+ nav: ["navigation", "menu", "navbar"],
13
+ input: ["field", "textbox", "text", "entry"],
14
+ email: ["mail", "e-mail"],
15
+ password: ["pass", "pwd", "secret"],
16
+ next: ["continue", "forward", "proceed"],
17
+ back: ["previous", "return", "go-back"],
18
+ save: ["store", "keep", "persist"],
19
+ delete: ["remove", "trash", "discard", "destroy"],
20
+ edit: ["modify", "change", "update"],
21
+ add: ["create", "new", "plus", "insert"],
22
+ settings: ["preferences", "config", "options", "gear"],
23
+ profile: ["account", "user", "avatar"],
24
+ home: ["main", "dashboard", "start"],
25
+ link: ["anchor", "href", "url"],
26
+ select: ["dropdown", "combo", "picker", "choose"],
27
+ checkbox: ["check", "toggle", "tick"],
28
+ upload: ["attach", "file", "browse"],
29
+ download: ["save", "export"]
30
+ };
31
+ var ROLE_KEYWORDS = /* @__PURE__ */ new Set([
32
+ "button",
33
+ "link",
34
+ "input",
35
+ "textbox",
36
+ "checkbox",
37
+ "radio",
38
+ "select",
39
+ "dropdown",
40
+ "tab",
41
+ "menu",
42
+ "menuitem",
43
+ "switch",
44
+ "slider",
45
+ "combobox",
46
+ "searchbox",
47
+ "option"
48
+ ]);
49
+ function tokenize(text) {
50
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((t) => t.length > 0);
51
+ }
52
+ function expandSynonyms(tokens) {
53
+ const expanded = new Set(tokens);
54
+ for (const token of tokens) {
55
+ const syns = SYNONYMS[token];
56
+ if (syns) {
57
+ for (const syn of syns) expanded.add(syn);
58
+ }
59
+ }
60
+ return expanded;
61
+ }
62
+ function freqMap(tokens) {
63
+ const map = /* @__PURE__ */ new Map();
64
+ for (const t of tokens) {
65
+ map.set(t, (map.get(t) || 0) + 1);
66
+ }
67
+ return map;
68
+ }
69
+ function jaccardScore(queryTokens, descTokens) {
70
+ const qFreq = freqMap(queryTokens);
71
+ const dFreq = freqMap(descTokens);
72
+ let intersection = 0;
73
+ let union = 0;
74
+ const allTokens = /* @__PURE__ */ new Set([...qFreq.keys(), ...dFreq.keys()]);
75
+ for (const token of allTokens) {
76
+ const qCount = qFreq.get(token) || 0;
77
+ const dCount = dFreq.get(token) || 0;
78
+ intersection += Math.min(qCount, dCount);
79
+ union += Math.max(qCount, dCount);
80
+ }
81
+ return union === 0 ? 0 : intersection / union;
82
+ }
83
+ function prefixScore(queryTokens, descTokens) {
84
+ if (queryTokens.length === 0 || descTokens.length === 0) return 0;
85
+ let matches = 0;
86
+ for (const qt of queryTokens) {
87
+ if (qt.length < 3) continue;
88
+ for (const dt of descTokens) {
89
+ if (dt.startsWith(qt) || qt.startsWith(dt)) {
90
+ matches += 0.5;
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ return Math.min(matches / queryTokens.length, 0.3);
96
+ }
97
+ function roleBoost(queryTokens, elementRole) {
98
+ const roleLower = elementRole.toLowerCase();
99
+ for (const qt of queryTokens) {
100
+ if (ROLE_KEYWORDS.has(qt) && roleLower.includes(qt)) {
101
+ return 0.2;
102
+ }
103
+ }
104
+ return 0;
105
+ }
106
+ function scoreElement(queryTokens, queryExpanded, element) {
107
+ const descParts = [
108
+ element.text,
109
+ element.role,
110
+ element.tag,
111
+ element.ariaLabel
112
+ ].filter(Boolean);
113
+ const descText = descParts.join(" ");
114
+ const descTokens = tokenize(descText);
115
+ if (descTokens.length === 0) return 0;
116
+ const descExpanded = expandSynonyms(descTokens);
117
+ const expandedQueryTokens = [...queryExpanded];
118
+ const expandedDescTokens = [...descExpanded];
119
+ const jaccard = jaccardScore(expandedQueryTokens, expandedDescTokens);
120
+ const prefix = prefixScore(queryTokens, descTokens);
121
+ const role = roleBoost(queryTokens, element.role || element.tag);
122
+ const queryStr = queryTokens.join(" ");
123
+ const descStr = descTokens.join(" ");
124
+ const exactBonus = descStr.includes(queryStr) ? 0.3 : 0;
125
+ return Math.min(jaccard + prefix + role + exactBonus, 1);
126
+ }
127
+ function semanticFind(elements, query, options) {
128
+ const maxResults = options?.maxResults ?? 5;
129
+ const minScore = options?.minScore ?? 0.3;
130
+ const queryTokens = tokenize(query);
131
+ if (queryTokens.length === 0) return [];
132
+ const queryExpanded = expandSynonyms(queryTokens);
133
+ const scored = [];
134
+ for (const el of elements) {
135
+ const score = scoreElement(queryTokens, queryExpanded, el);
136
+ if (score >= minScore) {
137
+ scored.push({
138
+ ref: el.index,
139
+ score: Math.round(score * 100) / 100,
140
+ text: (el.text || el.ariaLabel || "").slice(0, 60),
141
+ role: el.role || el.tag,
142
+ tag: el.tag
143
+ });
144
+ }
145
+ }
146
+ scored.sort((a, b) => b.score - a.score);
147
+ return scored.slice(0, maxResults);
148
+ }
149
+ export {
150
+ semanticFind
151
+ };
152
+ //# sourceMappingURL=semantic-find.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/browser/semantic-find.ts"],"sourcesContent":["/**\n * Semantic Element Finding — match elements by natural language.\n *\n * Uses Jaccard similarity with synonym expansion, role boost,\n * and prefix matching. Zero external dependencies — runs in Node.\n *\n * Inspired by PinchTab's hybrid lexical+embedding matcher, built from scratch.\n */\n\nexport interface FindMatch {\n ref: number;\n score: number;\n text: string;\n role: string;\n tag: string;\n}\n\nexport interface FindOptions {\n maxResults?: number; // default 5\n minScore?: number; // default 0.3\n}\n\ninterface InteractiveElement {\n index: number;\n tag: string;\n role: string;\n text: string;\n types: string[];\n ariaLabel: string;\n}\n\n// ── Synonym table ──\nconst SYNONYMS: Record<string, string[]> = {\n btn: ['button'],\n button: ['btn', 'submit', 'click'],\n submit: ['go', 'send', 'ok', 'confirm', 'done', 'button'],\n search: ['find', 'lookup', 'query', 'filter'],\n login: ['signin', 'sign-in', 'log-in', 'authenticate'],\n signup: ['register', 'create-account', 'sign-up', 'join'],\n logout: ['signout', 'sign-out', 'log-out'],\n close: ['dismiss', 'x', 'cancel', 'exit'],\n menu: ['nav', 'navigation', 'hamburger', 'sidebar'],\n nav: ['navigation', 'menu', 'navbar'],\n input: ['field', 'textbox', 'text', 'entry'],\n email: ['mail', 'e-mail'],\n password: ['pass', 'pwd', 'secret'],\n next: ['continue', 'forward', 'proceed'],\n back: ['previous', 'return', 'go-back'],\n save: ['store', 'keep', 'persist'],\n delete: ['remove', 'trash', 'discard', 'destroy'],\n edit: ['modify', 'change', 'update'],\n add: ['create', 'new', 'plus', 'insert'],\n settings: ['preferences', 'config', 'options', 'gear'],\n profile: ['account', 'user', 'avatar'],\n home: ['main', 'dashboard', 'start'],\n link: ['anchor', 'href', 'url'],\n select: ['dropdown', 'combo', 'picker', 'choose'],\n checkbox: ['check', 'toggle', 'tick'],\n upload: ['attach', 'file', 'browse'],\n download: ['save', 'export'],\n};\n\n// ── Role keywords that boost score ──\nconst ROLE_KEYWORDS = new Set([\n 'button', 'link', 'input', 'textbox', 'checkbox', 'radio',\n 'select', 'dropdown', 'tab', 'menu', 'menuitem', 'switch',\n 'slider', 'combobox', 'searchbox', 'option',\n]);\n\n/**\n * Tokenize a string into lowercase words.\n */\nfunction tokenize(text: string): string[] {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, ' ')\n .split(/[\\s-]+/)\n .filter((t) => t.length > 0);\n}\n\n/**\n * Expand tokens with synonyms.\n */\nfunction expandSynonyms(tokens: string[]): Set<string> {\n const expanded = new Set(tokens);\n for (const token of tokens) {\n const syns = SYNONYMS[token];\n if (syns) {\n for (const syn of syns) expanded.add(syn);\n }\n }\n return expanded;\n}\n\n/**\n * Build frequency map.\n */\nfunction freqMap(tokens: string[]): Map<string, number> {\n const map = new Map<string, number>();\n for (const t of tokens) {\n map.set(t, (map.get(t) || 0) + 1);\n }\n return map;\n}\n\n/**\n * Jaccard similarity with frequency weighting.\n */\nfunction jaccardScore(queryTokens: string[], descTokens: string[]): number {\n const qFreq = freqMap(queryTokens);\n const dFreq = freqMap(descTokens);\n\n let intersection = 0;\n let union = 0;\n\n const allTokens = new Set([...qFreq.keys(), ...dFreq.keys()]);\n for (const token of allTokens) {\n const qCount = qFreq.get(token) || 0;\n const dCount = dFreq.get(token) || 0;\n intersection += Math.min(qCount, dCount);\n union += Math.max(qCount, dCount);\n }\n\n return union === 0 ? 0 : intersection / union;\n}\n\n/**\n * Prefix matching score — handles abbreviations.\n * \"btn\" matches \"button\" partially.\n */\nfunction prefixScore(queryTokens: string[], descTokens: string[]): number {\n if (queryTokens.length === 0 || descTokens.length === 0) return 0;\n\n let matches = 0;\n for (const qt of queryTokens) {\n if (qt.length < 3) continue;\n for (const dt of descTokens) {\n if (dt.startsWith(qt) || qt.startsWith(dt)) {\n matches += 0.5;\n break;\n }\n }\n }\n\n return Math.min(matches / queryTokens.length, 0.3);\n}\n\n/**\n * Role keyword boost — if query mentions a role and element matches.\n */\nfunction roleBoost(queryTokens: string[], elementRole: string): number {\n const roleLower = elementRole.toLowerCase();\n for (const qt of queryTokens) {\n if (ROLE_KEYWORDS.has(qt) && roleLower.includes(qt)) {\n return 0.2;\n }\n }\n return 0;\n}\n\n/**\n * Score a single element against the query.\n */\nfunction scoreElement(\n queryTokens: string[],\n queryExpanded: Set<string>,\n element: InteractiveElement,\n): number {\n // Build description from all element text sources\n const descParts = [\n element.text,\n element.role,\n element.tag,\n element.ariaLabel,\n ].filter(Boolean);\n const descText = descParts.join(' ');\n const descTokens = tokenize(descText);\n\n if (descTokens.length === 0) return 0;\n\n // Expand description tokens too\n const descExpanded = expandSynonyms(descTokens);\n\n // 1. Jaccard similarity on expanded token sets\n const expandedQueryTokens = [...queryExpanded];\n const expandedDescTokens = [...descExpanded];\n const jaccard = jaccardScore(expandedQueryTokens, expandedDescTokens);\n\n // 2. Prefix matching\n const prefix = prefixScore(queryTokens, descTokens);\n\n // 3. Role keyword boost\n const role = roleBoost(queryTokens, element.role || element.tag);\n\n // 4. Exact substring match bonus\n const queryStr = queryTokens.join(' ');\n const descStr = descTokens.join(' ');\n const exactBonus = descStr.includes(queryStr) ? 0.3 : 0;\n\n return Math.min(jaccard + prefix + role + exactBonus, 1.0);\n}\n\n/**\n * Find elements matching a natural language query.\n *\n * @param elements - Interactive elements from INTERACTIVE_ELEMENTS_SCRIPT\n * @param query - Natural language description (e.g., \"login button\")\n * @param options - maxResults (default 5), minScore (default 0.3)\n */\nexport function semanticFind(\n elements: InteractiveElement[],\n query: string,\n options?: FindOptions,\n): FindMatch[] {\n const maxResults = options?.maxResults ?? 5;\n const minScore = options?.minScore ?? 0.3;\n\n const queryTokens = tokenize(query);\n if (queryTokens.length === 0) return [];\n\n const queryExpanded = expandSynonyms(queryTokens);\n\n const scored: FindMatch[] = [];\n\n for (const el of elements) {\n const score = scoreElement(queryTokens, queryExpanded, el);\n if (score >= minScore) {\n scored.push({\n ref: el.index,\n score: Math.round(score * 100) / 100,\n text: (el.text || el.ariaLabel || '').slice(0, 60),\n role: el.role || el.tag,\n tag: el.tag,\n });\n }\n }\n\n scored.sort((a, b) => b.score - a.score);\n return scored.slice(0, maxResults);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,187 @@
1
+ // src/browser/stealth.ts
2
+ var STEALTH_SCRIPT = `
3
+ (() => {
4
+ // \u2500\u2500 1. navigator.webdriver removal \u2500\u2500
5
+ // Most important: this is the #1 detection vector
6
+ Object.defineProperty(navigator, 'webdriver', {
7
+ get: () => undefined,
8
+ configurable: true,
9
+ });
10
+
11
+ // Also delete from prototype
12
+ delete Object.getPrototypeOf(navigator).webdriver;
13
+
14
+ // \u2500\u2500 2. CDP marker removal \u2500\u2500
15
+ // Chrome DevTools Protocol injects cdc_* properties on window
16
+ for (const key of Object.keys(window)) {
17
+ if (/^cdc_|^__webdriver|^__selenium|^__driver/.test(key)) {
18
+ try { delete window[key]; } catch {}
19
+ }
20
+ }
21
+
22
+ // \u2500\u2500 3. Chrome runtime spoofing \u2500\u2500
23
+ // Real Chrome has window.chrome with runtime, loadTimes, csi
24
+ if (!window.chrome) {
25
+ window.chrome = {};
26
+ }
27
+ if (!window.chrome.runtime) {
28
+ window.chrome.runtime = {
29
+ connect: function() {},
30
+ sendMessage: function() {},
31
+ onMessage: { addListener: function() {} },
32
+ id: undefined,
33
+ };
34
+ }
35
+ if (!window.chrome.loadTimes) {
36
+ window.chrome.loadTimes = function() {
37
+ return {
38
+ commitLoadTime: Date.now() / 1000 - 0.5,
39
+ connectionInfo: 'h2',
40
+ finishDocumentLoadTime: Date.now() / 1000 - 0.1,
41
+ finishLoadTime: Date.now() / 1000 - 0.05,
42
+ firstPaintAfterLoadTime: 0,
43
+ firstPaintTime: Date.now() / 1000 - 0.3,
44
+ navigationType: 'Other',
45
+ npnNegotiatedProtocol: 'h2',
46
+ requestTime: Date.now() / 1000 - 1,
47
+ startLoadTime: Date.now() / 1000 - 0.8,
48
+ wasAlternateProtocolAvailable: false,
49
+ wasFetchedViaSpdy: true,
50
+ wasNpnNegotiated: true,
51
+ };
52
+ };
53
+ }
54
+ if (!window.chrome.csi) {
55
+ window.chrome.csi = function() {
56
+ return {
57
+ onloadT: Date.now(),
58
+ startE: Date.now() - 500,
59
+ pageT: 500,
60
+ tran: 15,
61
+ };
62
+ };
63
+ }
64
+
65
+ // \u2500\u2500 4. Plugin array spoofing \u2500\u2500
66
+ // Headless Chrome reports empty plugins; real Chrome has at least 2
67
+ const fakePlugins = [
68
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
69
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
70
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
71
+ ];
72
+
73
+ Object.defineProperty(navigator, 'plugins', {
74
+ get: () => {
75
+ const arr = fakePlugins.map(p => {
76
+ const plugin = { ...p, item: (i) => plugin, namedItem: (n) => plugin };
77
+ return plugin;
78
+ });
79
+ arr.item = (i) => arr[i];
80
+ arr.namedItem = (n) => arr.find(p => p.name === n);
81
+ arr.refresh = () => {};
82
+ return arr;
83
+ },
84
+ });
85
+
86
+ // \u2500\u2500 5. Languages \u2500\u2500
87
+ Object.defineProperty(navigator, 'languages', {
88
+ get: () => ['en-US', 'en'],
89
+ });
90
+ Object.defineProperty(navigator, 'language', {
91
+ get: () => 'en-US',
92
+ });
93
+
94
+ // \u2500\u2500 6. Platform consistency \u2500\u2500
95
+ // Ensure platform matches user agent
96
+ const platform = navigator.userAgent.includes('Mac') ? 'MacIntel' :
97
+ navigator.userAgent.includes('Win') ? 'Win32' :
98
+ navigator.userAgent.includes('Linux') ? 'Linux x86_64' : navigator.platform;
99
+ Object.defineProperty(navigator, 'platform', { get: () => platform });
100
+
101
+ // \u2500\u2500 7. Hardware concurrency & device memory \u2500\u2500
102
+ // Headless often reports unusual values
103
+ if (navigator.hardwareConcurrency < 2) {
104
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
105
+ }
106
+ if (!navigator.deviceMemory || navigator.deviceMemory < 2) {
107
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
108
+ }
109
+
110
+ // \u2500\u2500 8. WebGL vendor/renderer spoofing \u2500\u2500
111
+ // Headless reports "Google SwiftShader" which is a dead giveaway
112
+ const origGetParameter = WebGLRenderingContext.prototype.getParameter;
113
+ WebGLRenderingContext.prototype.getParameter = function(param) {
114
+ // UNMASKED_VENDOR_WEBGL
115
+ if (param === 0x9245) return 'Intel Inc.';
116
+ // UNMASKED_RENDERER_WEBGL
117
+ if (param === 0x9246) return 'Intel Iris OpenGL Engine';
118
+ return origGetParameter.call(this, param);
119
+ };
120
+
121
+ // Also for WebGL2
122
+ if (typeof WebGL2RenderingContext !== 'undefined') {
123
+ const origGetParameter2 = WebGL2RenderingContext.prototype.getParameter;
124
+ WebGL2RenderingContext.prototype.getParameter = function(param) {
125
+ if (param === 0x9245) return 'Intel Inc.';
126
+ if (param === 0x9246) return 'Intel Iris OpenGL Engine';
127
+ return origGetParameter2.call(this, param);
128
+ };
129
+ }
130
+
131
+ // \u2500\u2500 9. Canvas fingerprint noise \u2500\u2500
132
+ // Adds subtle deterministic noise to canvas output based on domain
133
+ const seed = location.hostname.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
134
+ const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
135
+ HTMLCanvasElement.prototype.toDataURL = function(type) {
136
+ const ctx = this.getContext('2d');
137
+ if (ctx && this.width > 0 && this.height > 0) {
138
+ try {
139
+ const imageData = ctx.getImageData(0, 0, 1, 1);
140
+ // Flip a single pixel with seeded noise
141
+ imageData.data[0] = (imageData.data[0] + seed) % 256;
142
+ ctx.putImageData(imageData, 0, 0);
143
+ } catch {}
144
+ }
145
+ return origToDataURL.apply(this, arguments);
146
+ };
147
+
148
+ // \u2500\u2500 10. Permissions API \u2500\u2500
149
+ // Headless returns 'denied' for notifications; real Chrome returns 'prompt'
150
+ const origQuery = navigator.permissions?.query?.bind(navigator.permissions);
151
+ if (origQuery) {
152
+ navigator.permissions.query = function(descriptor) {
153
+ if (descriptor.name === 'notifications') {
154
+ return Promise.resolve({ state: Notification.permission || 'prompt', onchange: null });
155
+ }
156
+ return origQuery(descriptor);
157
+ };
158
+ }
159
+
160
+ // \u2500\u2500 11. Notification constructor \u2500\u2500
161
+ if (!window.Notification) {
162
+ window.Notification = function() {};
163
+ window.Notification.permission = 'default';
164
+ window.Notification.requestPermission = () => Promise.resolve('default');
165
+ }
166
+
167
+ // \u2500\u2500 12. Connection type \u2500\u2500
168
+ if (navigator.connection) {
169
+ Object.defineProperty(navigator.connection, 'rtt', { get: () => 50 });
170
+ }
171
+ })()
172
+ `;
173
+ async function injectStealth(page) {
174
+ await page.evaluateOnNewDocument(STEALTH_SCRIPT);
175
+ }
176
+ var STEALTH_ARGS = [
177
+ "--disable-blink-features=AutomationControlled",
178
+ "--disable-features=IsolateOrigins,site-per-process",
179
+ "--disable-infobars",
180
+ "--window-size=1920,1080"
181
+ ];
182
+ export {
183
+ STEALTH_ARGS,
184
+ STEALTH_SCRIPT,
185
+ injectStealth
186
+ };
187
+ //# sourceMappingURL=stealth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/browser/stealth.ts"],"sourcesContent":["/**\n * Stealth Mode — anti-bot detection scripts.\n *\n * Injected via page.evaluateOnNewDocument() so it runs before\n * any page JavaScript, on every navigation.\n *\n * Inspired by PinchTab's 3-tier stealth system, built from scratch.\n * This is a comprehensive single-tier implementation covering the\n * most critical detection vectors.\n */\n\nimport type { Page } from 'puppeteer-core';\n\n/**\n * Comprehensive stealth script that evades common bot detection.\n */\nexport const STEALTH_SCRIPT = `\n(() => {\n // ── 1. navigator.webdriver removal ──\n // Most important: this is the #1 detection vector\n Object.defineProperty(navigator, 'webdriver', {\n get: () => undefined,\n configurable: true,\n });\n\n // Also delete from prototype\n delete Object.getPrototypeOf(navigator).webdriver;\n\n // ── 2. CDP marker removal ──\n // Chrome DevTools Protocol injects cdc_* properties on window\n for (const key of Object.keys(window)) {\n if (/^cdc_|^__webdriver|^__selenium|^__driver/.test(key)) {\n try { delete window[key]; } catch {}\n }\n }\n\n // ── 3. Chrome runtime spoofing ──\n // Real Chrome has window.chrome with runtime, loadTimes, csi\n if (!window.chrome) {\n window.chrome = {};\n }\n if (!window.chrome.runtime) {\n window.chrome.runtime = {\n connect: function() {},\n sendMessage: function() {},\n onMessage: { addListener: function() {} },\n id: undefined,\n };\n }\n if (!window.chrome.loadTimes) {\n window.chrome.loadTimes = function() {\n return {\n commitLoadTime: Date.now() / 1000 - 0.5,\n connectionInfo: 'h2',\n finishDocumentLoadTime: Date.now() / 1000 - 0.1,\n finishLoadTime: Date.now() / 1000 - 0.05,\n firstPaintAfterLoadTime: 0,\n firstPaintTime: Date.now() / 1000 - 0.3,\n navigationType: 'Other',\n npnNegotiatedProtocol: 'h2',\n requestTime: Date.now() / 1000 - 1,\n startLoadTime: Date.now() / 1000 - 0.8,\n wasAlternateProtocolAvailable: false,\n wasFetchedViaSpdy: true,\n wasNpnNegotiated: true,\n };\n };\n }\n if (!window.chrome.csi) {\n window.chrome.csi = function() {\n return {\n onloadT: Date.now(),\n startE: Date.now() - 500,\n pageT: 500,\n tran: 15,\n };\n };\n }\n\n // ── 4. Plugin array spoofing ──\n // Headless Chrome reports empty plugins; real Chrome has at least 2\n const fakePlugins = [\n { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },\n { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },\n { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },\n ];\n\n Object.defineProperty(navigator, 'plugins', {\n get: () => {\n const arr = fakePlugins.map(p => {\n const plugin = { ...p, item: (i) => plugin, namedItem: (n) => plugin };\n return plugin;\n });\n arr.item = (i) => arr[i];\n arr.namedItem = (n) => arr.find(p => p.name === n);\n arr.refresh = () => {};\n return arr;\n },\n });\n\n // ── 5. Languages ──\n Object.defineProperty(navigator, 'languages', {\n get: () => ['en-US', 'en'],\n });\n Object.defineProperty(navigator, 'language', {\n get: () => 'en-US',\n });\n\n // ── 6. Platform consistency ──\n // Ensure platform matches user agent\n const platform = navigator.userAgent.includes('Mac') ? 'MacIntel' :\n navigator.userAgent.includes('Win') ? 'Win32' :\n navigator.userAgent.includes('Linux') ? 'Linux x86_64' : navigator.platform;\n Object.defineProperty(navigator, 'platform', { get: () => platform });\n\n // ── 7. Hardware concurrency & device memory ──\n // Headless often reports unusual values\n if (navigator.hardwareConcurrency < 2) {\n Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });\n }\n if (!navigator.deviceMemory || navigator.deviceMemory < 2) {\n Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });\n }\n\n // ── 8. WebGL vendor/renderer spoofing ──\n // Headless reports \"Google SwiftShader\" which is a dead giveaway\n const origGetParameter = WebGLRenderingContext.prototype.getParameter;\n WebGLRenderingContext.prototype.getParameter = function(param) {\n // UNMASKED_VENDOR_WEBGL\n if (param === 0x9245) return 'Intel Inc.';\n // UNMASKED_RENDERER_WEBGL\n if (param === 0x9246) return 'Intel Iris OpenGL Engine';\n return origGetParameter.call(this, param);\n };\n\n // Also for WebGL2\n if (typeof WebGL2RenderingContext !== 'undefined') {\n const origGetParameter2 = WebGL2RenderingContext.prototype.getParameter;\n WebGL2RenderingContext.prototype.getParameter = function(param) {\n if (param === 0x9245) return 'Intel Inc.';\n if (param === 0x9246) return 'Intel Iris OpenGL Engine';\n return origGetParameter2.call(this, param);\n };\n }\n\n // ── 9. Canvas fingerprint noise ──\n // Adds subtle deterministic noise to canvas output based on domain\n const seed = location.hostname.split('').reduce((a, c) => a + c.charCodeAt(0), 0);\n const origToDataURL = HTMLCanvasElement.prototype.toDataURL;\n HTMLCanvasElement.prototype.toDataURL = function(type) {\n const ctx = this.getContext('2d');\n if (ctx && this.width > 0 && this.height > 0) {\n try {\n const imageData = ctx.getImageData(0, 0, 1, 1);\n // Flip a single pixel with seeded noise\n imageData.data[0] = (imageData.data[0] + seed) % 256;\n ctx.putImageData(imageData, 0, 0);\n } catch {}\n }\n return origToDataURL.apply(this, arguments);\n };\n\n // ── 10. Permissions API ──\n // Headless returns 'denied' for notifications; real Chrome returns 'prompt'\n const origQuery = navigator.permissions?.query?.bind(navigator.permissions);\n if (origQuery) {\n navigator.permissions.query = function(descriptor) {\n if (descriptor.name === 'notifications') {\n return Promise.resolve({ state: Notification.permission || 'prompt', onchange: null });\n }\n return origQuery(descriptor);\n };\n }\n\n // ── 11. Notification constructor ──\n if (!window.Notification) {\n window.Notification = function() {};\n window.Notification.permission = 'default';\n window.Notification.requestPermission = () => Promise.resolve('default');\n }\n\n // ── 12. Connection type ──\n if (navigator.connection) {\n Object.defineProperty(navigator.connection, 'rtt', { get: () => 50 });\n }\n})()\n`;\n\n/**\n * Inject stealth script into a Puppeteer page.\n * Must be called before first navigation for full effectiveness.\n */\nexport async function injectStealth(page: Page): Promise<void> {\n await page.evaluateOnNewDocument(STEALTH_SCRIPT);\n}\n\n/**\n * Chrome launch args for stealth mode.\n */\nexport const STEALTH_ARGS = [\n '--disable-blink-features=AutomationControlled',\n '--disable-features=IsolateOrigins,site-per-process',\n '--disable-infobars',\n '--window-size=1920,1080',\n];\n"],"mappings":";AAgBO,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgL9B,eAAsB,cAAc,MAA2B;AAC7D,QAAM,KAAK,sBAAsB,cAAc;AACjD;AAKO,IAAM,eAAe;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}