@pablovitasso/szkrabok 1.0.10
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 +114 -0
- package/package.json +124 -0
- package/packages/runtime/config.js +173 -0
- package/packages/runtime/index.js +10 -0
- package/packages/runtime/launch.js +240 -0
- package/packages/runtime/logger.js +42 -0
- package/packages/runtime/mcp-client/adapters/szkrabok-session.js +69 -0
- package/packages/runtime/mcp-client/codegen/generate-mcp-tools.mjs +66 -0
- package/packages/runtime/mcp-client/codegen/render-tools.js +219 -0
- package/packages/runtime/mcp-client/codegen/schema-to-jsdoc.js +60 -0
- package/packages/runtime/mcp-client/mcp-tools.d.ts +92 -0
- package/packages/runtime/mcp-client/mcp-tools.js +99 -0
- package/packages/runtime/mcp-client/runtime/invoker.js +95 -0
- package/packages/runtime/mcp-client/runtime/logger.js +145 -0
- package/packages/runtime/mcp-client/runtime/transport.js +35 -0
- package/packages/runtime/package.json +25 -0
- package/packages/runtime/pool.js +59 -0
- package/packages/runtime/scripts/patch-playwright.js +736 -0
- package/packages/runtime/sessions.js +77 -0
- package/packages/runtime/stealth.js +232 -0
- package/packages/runtime/storage.js +64 -0
- package/scripts/detect_browsers.sh +147 -0
- package/scripts/patch-playwright.js +736 -0
- package/scripts/postinstall.js +47 -0
- package/scripts/release-publish.js +19 -0
- package/scripts/release-reminder.js +14 -0
- package/scripts/setup.js +17 -0
- package/src/cli.js +166 -0
- package/src/config.js +36 -0
- package/src/index.js +53 -0
- package/src/server.js +40 -0
- package/src/tools/registry.js +171 -0
- package/src/tools/scaffold.js +133 -0
- package/src/tools/szkrabok_browser.js +227 -0
- package/src/tools/szkrabok_session.js +174 -0
- package/src/tools/templates/automation/example.mcp.spec.js +54 -0
- package/src/tools/templates/automation/example.spec.js +29 -0
- package/src/tools/templates/automation/fixtures.js +59 -0
- package/src/tools/templates/playwright.config.js +10 -0
- package/src/tools/templates/szkrabok.config.local.toml.example +12 -0
- package/src/tools/workflow.js +45 -0
- package/src/utils/errors.js +36 -0
- package/src/utils/logger.js +64 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Session management helpers for callers that don't hold the launch() handle.
|
|
2
|
+
|
|
3
|
+
import * as pool from './pool.js';
|
|
4
|
+
import * as storage from './storage.js';
|
|
5
|
+
import { log } from './logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Close a session by profile name.
|
|
9
|
+
* Saves state, closes the context, removes from pool.
|
|
10
|
+
*/
|
|
11
|
+
export const closeSession = async profile => {
|
|
12
|
+
try {
|
|
13
|
+
const session = pool.get(profile);
|
|
14
|
+
|
|
15
|
+
const state = await session.context.storageState();
|
|
16
|
+
await storage.saveState(profile, state);
|
|
17
|
+
await storage.updateMeta(profile, { lastUsed: Date.now() });
|
|
18
|
+
await session.context.close();
|
|
19
|
+
pool.remove(profile);
|
|
20
|
+
|
|
21
|
+
return { success: true, profile };
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (pool.has(profile)) pool.remove(profile);
|
|
24
|
+
|
|
25
|
+
if (err.message?.includes('closed')) {
|
|
26
|
+
log(`Session ${profile} was already closed`);
|
|
27
|
+
return { success: true, profile, alreadyClosed: true };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a session entry from the pool.
|
|
36
|
+
* Returns { context, page, cdpPort, preset, label, createdAt }.
|
|
37
|
+
*/
|
|
38
|
+
export const getSession = profile => pool.get(profile);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List all active sessions in pool.
|
|
42
|
+
*/
|
|
43
|
+
export const listSessions = () => pool.list();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List all session IDs stored on disk (active or inactive).
|
|
47
|
+
*/
|
|
48
|
+
export const listStoredSessions = () => storage.listSessions();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Update session metadata on disk (e.g. lastUrl after navigation).
|
|
52
|
+
*/
|
|
53
|
+
export const updateSessionMeta = (profile, updates) => storage.updateMeta(profile, updates);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Delete a session's storage from disk (profile dir, state.json, meta.json).
|
|
57
|
+
* If the session is open, it is closed first.
|
|
58
|
+
*/
|
|
59
|
+
export const deleteStoredSession = async profile => {
|
|
60
|
+
if (pool.has(profile)) {
|
|
61
|
+
await closeSession(profile).catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
await storage.deleteSession(profile);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Close all open sessions (used on server shutdown).
|
|
68
|
+
*/
|
|
69
|
+
export const closeAllSessions = () => pool.closeAll();
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update the active page for a session (e.g. after tabs.select switches focus).
|
|
73
|
+
*/
|
|
74
|
+
export const updateSessionPage = (profile, page) => {
|
|
75
|
+
const session = pool.get(profile);
|
|
76
|
+
pool.add(profile, session.context, page, session.cdpPort, session.preset, session.label);
|
|
77
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { addExtra } from 'playwright-extra';
|
|
2
|
+
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
3
|
+
import UserAgentOverride from 'puppeteer-extra-plugin-stealth/evasions/user-agent-override/index.js';
|
|
4
|
+
import NavigatorVendor from 'puppeteer-extra-plugin-stealth/evasions/navigator.vendor/index.js';
|
|
5
|
+
import NavigatorHardwareConcurrency from 'puppeteer-extra-plugin-stealth/evasions/navigator.hardwareConcurrency/index.js';
|
|
6
|
+
import NavigatorLanguages from 'puppeteer-extra-plugin-stealth/evasions/navigator.languages/index.js';
|
|
7
|
+
import WebGLVendor from 'puppeteer-extra-plugin-stealth/evasions/webgl.vendor/index.js';
|
|
8
|
+
import { STEALTH_CONFIG } from './config.js';
|
|
9
|
+
import { log, logDebug } from './logger.js';
|
|
10
|
+
|
|
11
|
+
const CONFIGURABLE_EVASIONS = [
|
|
12
|
+
'user-agent-override',
|
|
13
|
+
'navigator.vendor',
|
|
14
|
+
'navigator.hardwareConcurrency',
|
|
15
|
+
'navigator.languages',
|
|
16
|
+
'webgl.vendor',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const enhanceWithStealth = (browser, presetConfig = {}) => {
|
|
20
|
+
log('Initializing puppeteer-extra-plugin-stealth');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const enhanced = addExtra(browser);
|
|
24
|
+
const stealth = StealthPlugin();
|
|
25
|
+
|
|
26
|
+
for (const name of CONFIGURABLE_EVASIONS) {
|
|
27
|
+
stealth.enabledEvasions.delete(name);
|
|
28
|
+
}
|
|
29
|
+
stealth.enabledEvasions.delete('user-data-dir');
|
|
30
|
+
|
|
31
|
+
for (const [name, enabled] of Object.entries(STEALTH_CONFIG.evasions)) {
|
|
32
|
+
if (enabled) {
|
|
33
|
+
stealth.enabledEvasions.add(name);
|
|
34
|
+
} else {
|
|
35
|
+
stealth.enabledEvasions.delete(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
enhanced.use(stealth);
|
|
40
|
+
|
|
41
|
+
const uaConfig = STEALTH_CONFIG['user-agent-override'];
|
|
42
|
+
const overrideUA = presetConfig.overrideUserAgent ?? uaConfig.enabled ?? true;
|
|
43
|
+
if (overrideUA) {
|
|
44
|
+
enhanced.use(
|
|
45
|
+
UserAgentOverride({
|
|
46
|
+
userAgent: presetConfig.userAgent || undefined,
|
|
47
|
+
locale: presetConfig.locale || undefined,
|
|
48
|
+
maskLinux: uaConfig.mask_linux ?? true,
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const vendorConfig = STEALTH_CONFIG['navigator.vendor'];
|
|
54
|
+
if (vendorConfig.enabled ?? true) {
|
|
55
|
+
enhanced.use(NavigatorVendor({ vendor: vendorConfig.vendor ?? 'Google Inc.' }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hwConfig = STEALTH_CONFIG['navigator.hardwareConcurrency'];
|
|
59
|
+
if (hwConfig.enabled ?? true) {
|
|
60
|
+
enhanced.use(NavigatorHardwareConcurrency({ hardwareConcurrency: hwConfig.hardware_concurrency ?? 4 }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const langConfig = STEALTH_CONFIG['navigator.languages'];
|
|
64
|
+
if (langConfig.enabled ?? true) {
|
|
65
|
+
const locale = presetConfig.locale || 'en-US';
|
|
66
|
+
const languages = langConfig.languages ?? [locale, locale.split('-')[0]].filter(Boolean);
|
|
67
|
+
enhanced.use(NavigatorLanguages({ languages }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const webglConfig = STEALTH_CONFIG['webgl.vendor'];
|
|
71
|
+
if (webglConfig.enabled ?? true) {
|
|
72
|
+
enhanced.use(
|
|
73
|
+
WebGLVendor({
|
|
74
|
+
vendor: webglConfig.vendor ?? 'Intel Inc.',
|
|
75
|
+
renderer: webglConfig.renderer ?? 'Intel Iris OpenGL Engine',
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return enhanced;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
log('Stealth plugin failed, using vanilla Playwright', err.message);
|
|
83
|
+
return browser;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const applyStealthToExistingPage = async (page, presetConfig = {}) => {
|
|
88
|
+
try {
|
|
89
|
+
logDebug('applyStealthToExistingPage called', { presetConfig });
|
|
90
|
+
const uaConfig = STEALTH_CONFIG['user-agent-override'];
|
|
91
|
+
const overrideUA = presetConfig.overrideUserAgent ?? uaConfig.enabled ?? true;
|
|
92
|
+
const hwConfig = STEALTH_CONFIG['navigator.hardwareConcurrency'];
|
|
93
|
+
const webglConfig = STEALTH_CONFIG['webgl.vendor'];
|
|
94
|
+
const langConfig = STEALTH_CONFIG['navigator.languages'];
|
|
95
|
+
|
|
96
|
+
const client = await page.context().newCDPSession(page);
|
|
97
|
+
|
|
98
|
+
if (overrideUA) {
|
|
99
|
+
const ua = presetConfig.userAgent || '';
|
|
100
|
+
const chromeMatch = ua.match(/Chrome\/([\d.]+)/);
|
|
101
|
+
const uaVersion = chromeMatch ? chromeMatch[1] : '120.0.0.0';
|
|
102
|
+
const seed = parseInt(uaVersion.split('.')[0]);
|
|
103
|
+
|
|
104
|
+
const order = [
|
|
105
|
+
[0, 1, 2],
|
|
106
|
+
[0, 2, 1],
|
|
107
|
+
[1, 0, 2],
|
|
108
|
+
[1, 2, 0],
|
|
109
|
+
[2, 0, 1],
|
|
110
|
+
[2, 1, 0],
|
|
111
|
+
][seed % 6];
|
|
112
|
+
const escapedChars = [' ', ' ', ';'];
|
|
113
|
+
const greaseyBrand = `${escapedChars[order[0]]}Not${escapedChars[order[1]]}A${escapedChars[order[2]]}Brand`;
|
|
114
|
+
const brands = [];
|
|
115
|
+
brands[order[0]] = { brand: greaseyBrand, version: '99' };
|
|
116
|
+
brands[order[1]] = { brand: 'Chromium', version: String(seed) };
|
|
117
|
+
brands[order[2]] = { brand: 'Google Chrome', version: String(seed) };
|
|
118
|
+
|
|
119
|
+
const maskLinux = uaConfig.mask_linux ?? true;
|
|
120
|
+
let platform = 'Win32';
|
|
121
|
+
let extPlatform = 'Windows';
|
|
122
|
+
let platformVersion = '10.0';
|
|
123
|
+
if (ua.includes('Mac OS X')) {
|
|
124
|
+
platform = 'MacIntel';
|
|
125
|
+
extPlatform = 'Mac OS X';
|
|
126
|
+
const macMatch = ua.match(/Mac OS X ([^)]+)/);
|
|
127
|
+
platformVersion = macMatch ? macMatch[1] : '10_15_7';
|
|
128
|
+
} else if (ua.includes('Android')) {
|
|
129
|
+
platform = 'Android';
|
|
130
|
+
extPlatform = 'Android';
|
|
131
|
+
const andMatch = ua.match(/Android ([^;]+)/);
|
|
132
|
+
platformVersion = andMatch ? andMatch[1] : '14';
|
|
133
|
+
} else if (ua.includes('Linux') && !maskLinux) {
|
|
134
|
+
platform = 'Linux x86_64';
|
|
135
|
+
extPlatform = 'Linux';
|
|
136
|
+
platformVersion = '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const locale = presetConfig.locale || 'en-US';
|
|
140
|
+
|
|
141
|
+
await client.send('Network.setUserAgentOverride', {
|
|
142
|
+
userAgent: ua,
|
|
143
|
+
acceptLanguage: `${locale},${locale.split('-')[0]};q=0.9`,
|
|
144
|
+
platform,
|
|
145
|
+
userAgentMetadata: {
|
|
146
|
+
brands,
|
|
147
|
+
fullVersion: uaVersion,
|
|
148
|
+
platform: extPlatform,
|
|
149
|
+
platformVersion,
|
|
150
|
+
architecture: 'x86',
|
|
151
|
+
model: '',
|
|
152
|
+
mobile: false,
|
|
153
|
+
bitness: '64',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const brandsJson = JSON.stringify(brands);
|
|
158
|
+
const fullVersion = uaVersion;
|
|
159
|
+
await page.addInitScript(`
|
|
160
|
+
(function() {
|
|
161
|
+
const _brands = ${brandsJson};
|
|
162
|
+
const _uad = {
|
|
163
|
+
brands: _brands,
|
|
164
|
+
mobile: false,
|
|
165
|
+
platform: 'Windows',
|
|
166
|
+
getHighEntropyValues: async (hints) => {
|
|
167
|
+
const result = {};
|
|
168
|
+
if (hints.includes('brands')) result.brands = _brands;
|
|
169
|
+
if (hints.includes('mobile')) result.mobile = false;
|
|
170
|
+
if (hints.includes('platform')) result.platform = 'Windows';
|
|
171
|
+
if (hints.includes('platformVersion')) result.platformVersion = '10.0.0';
|
|
172
|
+
if (hints.includes('architecture')) result.architecture = 'x86';
|
|
173
|
+
if (hints.includes('bitness')) result.bitness = '64';
|
|
174
|
+
if (hints.includes('model')) result.model = '';
|
|
175
|
+
if (hints.includes('uaFullVersion')) result.uaFullVersion = ${JSON.stringify(fullVersion)};
|
|
176
|
+
if (hints.includes('fullVersionList')) result.fullVersionList = _brands.map(b => ({ brand: b.brand, version: b.version + '.0.0.0' }));
|
|
177
|
+
return result;
|
|
178
|
+
},
|
|
179
|
+
toJSON: () => ({ brands: _brands, mobile: false, platform: 'Windows' }),
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
Object.defineProperty(Navigator.prototype, 'userAgentData', { get: () => _uad, configurable: true });
|
|
183
|
+
} catch(e) {}
|
|
184
|
+
})();`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (hwConfig.enabled ?? true) {
|
|
188
|
+
const concurrency = hwConfig.hardware_concurrency ?? 4;
|
|
189
|
+
logDebug('registering hardwareConcurrency init script', { concurrency });
|
|
190
|
+
await page.addInitScript(`(function(){
|
|
191
|
+
try {
|
|
192
|
+
Object.defineProperty(Navigator.prototype, 'hardwareConcurrency', { get: () => ${concurrency}, configurable: true });
|
|
193
|
+
} catch(e) {}
|
|
194
|
+
})()`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (langConfig.enabled ?? true) {
|
|
198
|
+
const locale = presetConfig.locale || 'en-US';
|
|
199
|
+
const languages = langConfig.languages ?? [locale, locale.split('-')[0]].filter(Boolean);
|
|
200
|
+
const langsJson = JSON.stringify(languages);
|
|
201
|
+
await page.addInitScript(
|
|
202
|
+
`Object.defineProperty(Navigator.prototype, 'languages', { get: () => ${langsJson}, configurable: true });`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (webglConfig.enabled ?? true) {
|
|
207
|
+
const vendor = webglConfig.vendor ?? 'Intel Inc.';
|
|
208
|
+
const renderer = webglConfig.renderer ?? 'Intel Iris OpenGL Engine';
|
|
209
|
+
await page.addInitScript(`
|
|
210
|
+
(function() {
|
|
211
|
+
const _getParameter = WebGLRenderingContext.prototype.getParameter;
|
|
212
|
+
WebGLRenderingContext.prototype.getParameter = function(p) {
|
|
213
|
+
if (p === 37445) return ${JSON.stringify(vendor)};
|
|
214
|
+
if (p === 37446) return ${JSON.stringify(renderer)};
|
|
215
|
+
return _getParameter.call(this, p);
|
|
216
|
+
};
|
|
217
|
+
if (typeof WebGL2RenderingContext !== 'undefined') {
|
|
218
|
+
const _get2 = WebGL2RenderingContext.prototype.getParameter;
|
|
219
|
+
WebGL2RenderingContext.prototype.getParameter = function(p) {
|
|
220
|
+
if (p === 37445) return ${JSON.stringify(vendor)};
|
|
221
|
+
if (p === 37446) return ${JSON.stringify(renderer)};
|
|
222
|
+
return _get2.call(this, p);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
})()`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
log('Applied stealth evasions to existing page via CDP');
|
|
229
|
+
} catch (err) {
|
|
230
|
+
log('applyStealthToExistingPage failed', err.message);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rm, readdir } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
// Sessions dir: SZKRABOK_SESSIONS_DIR env > cwd/sessions
|
|
6
|
+
const getSessionsDir = () =>
|
|
7
|
+
process.env.SZKRABOK_SESSIONS_DIR ?? join(process.cwd(), 'sessions');
|
|
8
|
+
|
|
9
|
+
const getSessionDir = id => join(getSessionsDir(), id);
|
|
10
|
+
const getStatePath = id => join(getSessionDir(id), 'state.json');
|
|
11
|
+
const getMetaPath = id => join(getSessionDir(id), 'meta.json');
|
|
12
|
+
|
|
13
|
+
export const getUserDataDir = id => join(getSessionDir(id), 'profile');
|
|
14
|
+
|
|
15
|
+
export const ensureSessionsDir = async () => {
|
|
16
|
+
const dir = getSessionsDir();
|
|
17
|
+
if (!existsSync(dir)) {
|
|
18
|
+
await mkdir(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const sessionExists = id => existsSync(getSessionDir(id));
|
|
23
|
+
|
|
24
|
+
export const loadState = async id => {
|
|
25
|
+
const path = getStatePath(id);
|
|
26
|
+
if (!existsSync(path)) return null;
|
|
27
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const saveState = async (id, state) => {
|
|
31
|
+
await mkdir(getSessionDir(id), { recursive: true });
|
|
32
|
+
await writeFile(getStatePath(id), JSON.stringify(state, null, 2));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const loadMeta = async id => {
|
|
36
|
+
const path = getMetaPath(id);
|
|
37
|
+
if (!existsSync(path)) return null;
|
|
38
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const saveMeta = async (id, meta) => {
|
|
42
|
+
await mkdir(getSessionDir(id), { recursive: true });
|
|
43
|
+
await writeFile(getMetaPath(id), JSON.stringify(meta, null, 2));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const updateMeta = async (id, updates) => {
|
|
47
|
+
const meta = (await loadMeta(id)) || {};
|
|
48
|
+
const updated = { ...meta, ...updates, lastUsed: Date.now() };
|
|
49
|
+
await saveMeta(id, updated);
|
|
50
|
+
return updated;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const deleteSession = async id => {
|
|
54
|
+
const dir = getSessionDir(id);
|
|
55
|
+
if (existsSync(dir)) {
|
|
56
|
+
await rm(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const listSessions = async () => {
|
|
61
|
+
await ensureSessionsDir();
|
|
62
|
+
const dirs = await readdir(getSessionsDir(), { withFileTypes: true });
|
|
63
|
+
return dirs.filter(d => d.isDirectory()).map(d => d.name);
|
|
64
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# detect_browsers.sh — find Chrome/Chromium binaries and echo szkrabok.config.toml lines
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash scripts/detect_browsers.sh
|
|
6
|
+
#
|
|
7
|
+
# Output:
|
|
8
|
+
# For each detected browser: its version + the TOML executablePath line to use.
|
|
9
|
+
# If nothing useful is found: installation suggestions.
|
|
10
|
+
#
|
|
11
|
+
# Copy the executablePath line you want into szkrabok.config.toml [default] section.
|
|
12
|
+
# Google Chrome stable is best for stealth (native brands). Ungoogled-chromium is a
|
|
13
|
+
# good second choice. The Playwright bundled binary (Chrome for Testing) should be
|
|
14
|
+
# avoided in production — it brands itself as automation tooling.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
FOUND=()
|
|
19
|
+
|
|
20
|
+
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
get_version() {
|
|
23
|
+
# Run binary with --version, strip leading "Chromium ", "Google Chrome " etc.
|
|
24
|
+
timeout 5 "$1" --version 2>/dev/null | head -1 | sed 's/^[^0-9]*//' || true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
check_binary() {
|
|
28
|
+
local label="$1" path="$2"
|
|
29
|
+
if [[ -x "$path" ]]; then
|
|
30
|
+
local ver
|
|
31
|
+
ver="$(get_version "$path")"
|
|
32
|
+
if [[ -n "$ver" ]]; then
|
|
33
|
+
echo " FOUND $label — $ver"
|
|
34
|
+
echo " executablePath = \"$path\""
|
|
35
|
+
echo ""
|
|
36
|
+
FOUND+=("$label")
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
check_flatpak() {
|
|
42
|
+
local label="$1" app_id="$2"
|
|
43
|
+
if command -v flatpak >/dev/null 2>&1 && flatpak info "$app_id" >/dev/null 2>&1; then
|
|
44
|
+
local ver
|
|
45
|
+
ver="$(timeout 5 flatpak run "$app_id" --version 2>/dev/null | head -1 | sed 's/^[^0-9]*//' || true)"
|
|
46
|
+
if [[ -n "$ver" ]]; then
|
|
47
|
+
# Write a small wrapper path check — flatpak run is the executable path
|
|
48
|
+
local wrapper
|
|
49
|
+
wrapper="$(command -v flatpak)"
|
|
50
|
+
echo " FOUND $label — $ver"
|
|
51
|
+
echo " # flatpak binary — use a wrapper script or the flatpak run command:"
|
|
52
|
+
echo " executablePath = \"$HOME/.local/bin/$(echo "$app_id" | tr '.' '-' | tr '[:upper:]' '[:lower:]')\""
|
|
53
|
+
echo " # (create that wrapper: echo '#!/bin/sh' > the path, then:"
|
|
54
|
+
echo " # echo 'exec flatpak run $app_id \"\$@\"' >> the path, chmod +x)"
|
|
55
|
+
echo ""
|
|
56
|
+
FOUND+=("$label")
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
check_snap() {
|
|
62
|
+
local label="$1" snap_name="$2" snap_bin="$3"
|
|
63
|
+
if [[ -x "$snap_bin" ]]; then
|
|
64
|
+
local ver
|
|
65
|
+
ver="$(get_version "$snap_bin")"
|
|
66
|
+
if [[ -n "$ver" ]]; then
|
|
67
|
+
echo " FOUND $label — $ver"
|
|
68
|
+
echo " executablePath = \"$snap_bin\""
|
|
69
|
+
echo ""
|
|
70
|
+
FOUND+=("$label")
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# ── scan ──────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
echo ""
|
|
78
|
+
echo "=== szkrabok browser detection ==="
|
|
79
|
+
echo ""
|
|
80
|
+
echo "Scanning for usable Chrome/Chromium binaries..."
|
|
81
|
+
echo ""
|
|
82
|
+
|
|
83
|
+
# Google Chrome stable (best for stealth — native brands)
|
|
84
|
+
check_binary "Google Chrome stable" "/usr/bin/google-chrome"
|
|
85
|
+
check_binary "Google Chrome stable" "/usr/bin/google-chrome-stable"
|
|
86
|
+
check_binary "Google Chrome stable" "/opt/google/chrome/chrome"
|
|
87
|
+
check_binary "Google Chrome stable" "/opt/google/chrome/google-chrome"
|
|
88
|
+
|
|
89
|
+
# Chromium (distro package)
|
|
90
|
+
check_binary "Chromium (distro)" "/usr/bin/chromium"
|
|
91
|
+
check_binary "Chromium (distro)" "/usr/bin/chromium-browser"
|
|
92
|
+
|
|
93
|
+
# Ungoogled-chromium — common wrapper locations
|
|
94
|
+
check_binary "Ungoogled-chromium" "$HOME/.local/bin/ungoogled-chromium"
|
|
95
|
+
check_binary "Ungoogled-chromium" "/usr/bin/ungoogled-chromium"
|
|
96
|
+
|
|
97
|
+
# Ungoogled-chromium via flatpak — only if no wrapper binary was found above
|
|
98
|
+
if [[ ! -x "$HOME/.local/bin/ungoogled-chromium" && ! -x "/usr/bin/ungoogled-chromium" ]]; then
|
|
99
|
+
check_flatpak "Ungoogled-chromium (flatpak)" "io.github.ungoogled_software.ungoogled_chromium"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Chromium via snap
|
|
103
|
+
check_snap "Chromium (snap)" "chromium" "/snap/bin/chromium"
|
|
104
|
+
|
|
105
|
+
# Brave
|
|
106
|
+
check_binary "Brave" "/usr/bin/brave-browser"
|
|
107
|
+
check_binary "Brave" "/usr/bin/brave-browser-stable"
|
|
108
|
+
check_binary "Brave" "/opt/brave.com/brave/brave"
|
|
109
|
+
|
|
110
|
+
# Microsoft Edge (Chromium-based)
|
|
111
|
+
check_binary "Microsoft Edge" "/usr/bin/microsoft-edge"
|
|
112
|
+
check_binary "Microsoft Edge" "/usr/bin/microsoft-edge-stable"
|
|
113
|
+
check_binary "Microsoft Edge" "/opt/microsoft/msedge/msedge"
|
|
114
|
+
|
|
115
|
+
# ── results ───────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
if [[ ${#FOUND[@]} -eq 0 ]]; then
|
|
118
|
+
echo " NONE FOUND — no usable Chrome/Chromium binary detected."
|
|
119
|
+
echo ""
|
|
120
|
+
echo " szkrabok will fall back to the Playwright bundled 'Chrome for Testing'."
|
|
121
|
+
echo " This works but is detectable as automation tooling (brands itself as"
|
|
122
|
+
echo " 'Chrome for Testing' in navigator.userAgentData)."
|
|
123
|
+
echo ""
|
|
124
|
+
echo " Recommended installations:"
|
|
125
|
+
echo ""
|
|
126
|
+
echo " 1. Google Chrome stable (best stealth — native brands):"
|
|
127
|
+
echo " https://www.google.com/chrome/"
|
|
128
|
+
echo " wget -qO- https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -"
|
|
129
|
+
echo " sudo sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" > /etc/apt/sources.list.d/google-chrome.list'"
|
|
130
|
+
echo " sudo apt-get update && sudo apt-get install google-chrome-stable"
|
|
131
|
+
echo ""
|
|
132
|
+
echo " 2. Ungoogled-chromium via flatpak (no Google services, good stealth):"
|
|
133
|
+
echo " flatpak install flathub io.github.ungoogled_software.ungoogled_chromium"
|
|
134
|
+
echo " # then create a wrapper: ~/.local/bin/ungoogled-chromium"
|
|
135
|
+
echo " # with: exec flatpak run io.github.ungoogled_software.ungoogled_chromium \"\$@\""
|
|
136
|
+
echo ""
|
|
137
|
+
else
|
|
138
|
+
echo " Copy one executablePath line above into szkrabok.config.toml [default]."
|
|
139
|
+
echo ""
|
|
140
|
+
echo " Stealth ranking (best first):"
|
|
141
|
+
echo " 1. Google Chrome stable — native 'Google Chrome' brands, most trusted"
|
|
142
|
+
echo " 2. Ungoogled-chromium — no Google services; needs greasy brands patch"
|
|
143
|
+
echo " 3. Chromium (distro) — same as ungoogled; needs greasy brands patch"
|
|
144
|
+
echo " 4. Chrome for Testing — avoid in production (automation-branded)"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
echo ""
|