@pablovitasso/szkrabok 1.0.18 → 1.0.19
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/package.json +3 -1
- package/packages/runtime/package.json +1 -1
- package/scripts/patch-playwright.js +15 -9
- package/scripts/resolve-playwright-core.js +24 -0
- package/scripts/smoke-test.js +102 -0
- package/src/cli/commands/detect-browser.js +22 -0
- package/src/cli/commands/doctor.js +89 -0
- package/src/cli/commands/endpoint.js +14 -0
- package/src/cli/commands/init.js +23 -0
- package/src/cli/commands/install-browser.js +11 -0
- package/src/cli/commands/open.js +25 -0
- package/src/cli/commands/session.js +91 -0
- package/src/cli/index.js +63 -0
- package/src/index.js +21 -2
- package/src/cli.js +0 -277
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pablovitasso/szkrabok",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "Production-grade MCP browser automation layer with persistent sessions and stealth capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"format:check": "prettier --check \"{src,tests}/**/*.{js,json,md}\" \"*.{js,json,md}\"",
|
|
37
37
|
"codegen:mcp": "node packages/runtime/mcp-client/codegen/generate-mcp-tools.mjs",
|
|
38
38
|
"prepack": "git describe --exact-match --tags HEAD 2>/dev/null || (echo 'ERROR: HEAD is not tagged. Run npm run release:patch or release:minor first.' && exit 1)",
|
|
39
|
+
"prepublishOnly": "node scripts/smoke-test.js",
|
|
39
40
|
"release:patch": "npm version patch --workspaces --include-workspace-root --ignore-scripts --no-git-tag-version && node scripts/release-commit.js",
|
|
40
41
|
"release:minor": "npm version minor --workspaces --include-workspace-root --ignore-scripts --no-git-tag-version && node scripts/release-commit.js",
|
|
41
42
|
"deps:update": "npx npm-check-updates -u --workspaces && npm install",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
},
|
|
70
71
|
"dependencies": {
|
|
71
72
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
73
|
+
"commander": "^14.0.3",
|
|
72
74
|
"dotenv": "^17.3.1",
|
|
73
75
|
"playwright": "^1.58.2",
|
|
74
76
|
"playwright-core": "^1.58.2",
|
|
@@ -94,8 +94,11 @@ import fs from 'fs'
|
|
|
94
94
|
import path from 'path'
|
|
95
95
|
import { execSync } from 'child_process'
|
|
96
96
|
import { createRequire } from 'module'
|
|
97
|
+
import { fileURLToPath } from 'url'
|
|
98
|
+
import { resolvePlaywrightCore } from './resolve-playwright-core.js'
|
|
97
99
|
|
|
98
100
|
const require = createRequire(import.meta.url)
|
|
101
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
99
102
|
|
|
100
103
|
// ── locate all playwright-core installs ───────────────────────────────────────
|
|
101
104
|
// npm hoists one copy to node_modules/playwright-core but playwright itself
|
|
@@ -105,22 +108,24 @@ const require = createRequire(import.meta.url)
|
|
|
105
108
|
|
|
106
109
|
function findPkgRoots() {
|
|
107
110
|
const roots = []
|
|
108
|
-
const
|
|
111
|
+
const pkgRoot = path.resolve(__dirname, '..')
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (fs.existsSync(path.join(top, 'package.json'))) roots.push(top)
|
|
113
|
+
const primary = resolvePlaywrightCore(pkgRoot, fs.existsSync.bind(fs), path)
|
|
114
|
+
if (primary) roots.push(primary)
|
|
113
115
|
|
|
114
|
-
//
|
|
116
|
+
// Also find playwright's own nested playwright-core copy
|
|
117
|
+
const enclosingNm = pkgRoot.includes('node_modules')
|
|
118
|
+
? pkgRoot.slice(0, pkgRoot.lastIndexOf('node_modules') + 'node_modules'.length)
|
|
119
|
+
: path.join(pkgRoot, 'node_modules')
|
|
115
120
|
try {
|
|
116
121
|
const out = execSync(
|
|
117
|
-
|
|
122
|
+
`find ${enclosingNm}/playwright -maxdepth 3 -name "package.json" -path "*/playwright-core/package.json" 2>/dev/null`,
|
|
118
123
|
{ encoding: 'utf8' }
|
|
119
124
|
)
|
|
120
125
|
for (const line of out.trim().split('\n')) {
|
|
121
126
|
if (!line) continue
|
|
122
|
-
const
|
|
123
|
-
if (!roots.includes(
|
|
127
|
+
const nested = path.dirname(path.resolve(line))
|
|
128
|
+
if (!roots.includes(nested)) roots.push(nested)
|
|
124
129
|
}
|
|
125
130
|
} catch {}
|
|
126
131
|
|
|
@@ -129,7 +134,8 @@ function findPkgRoots() {
|
|
|
129
134
|
|
|
130
135
|
const pkgRoots = findPkgRoots()
|
|
131
136
|
if (!pkgRoots.length) {
|
|
132
|
-
console.error('[patch-playwright] ERROR: playwright-core not found
|
|
137
|
+
console.error('[patch-playwright] ERROR: playwright-core not found.')
|
|
138
|
+
console.error(` Searched: ${path.resolve(__dirname, '..', 'node_modules', 'playwright-core')}`)
|
|
133
139
|
console.error(' Run `npm install` first.')
|
|
134
140
|
process.exit(1)
|
|
135
141
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility: find playwright-core root relative to a given package root.
|
|
3
|
+
* Handles npm hoisting — playwright-core may be in an ancestor node_modules dir.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} pkgRoot - absolute path to the package root (dir containing package.json)
|
|
6
|
+
* @param {function} existsSync - fs.existsSync
|
|
7
|
+
* @param {object} path - node:path module (needs join)
|
|
8
|
+
* @returns {string|null} absolute path to playwright-core dir, or null if not found
|
|
9
|
+
*/
|
|
10
|
+
export function resolvePlaywrightCore(pkgRoot, existsSync, path) {
|
|
11
|
+
// When installed via npm, playwright-core is hoisted to the enclosing node_modules.
|
|
12
|
+
const enclosingNm = pkgRoot.includes('node_modules')
|
|
13
|
+
? pkgRoot.slice(0, pkgRoot.lastIndexOf('node_modules') + 'node_modules'.length)
|
|
14
|
+
: path.join(pkgRoot, 'node_modules');
|
|
15
|
+
|
|
16
|
+
const hoisted = path.join(enclosingNm, 'playwright-core');
|
|
17
|
+
if (existsSync(path.join(hoisted, 'package.json'))) return hoisted;
|
|
18
|
+
|
|
19
|
+
// Fallback: own nested node_modules (non-hoisted or dev workspace)
|
|
20
|
+
const own = path.join(pkgRoot, 'node_modules', 'playwright-core');
|
|
21
|
+
if (existsSync(path.join(own, 'package.json'))) return own;
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* smoke-test.js — run as prepublishOnly to catch packaging bugs before npm publish.
|
|
4
|
+
*
|
|
5
|
+
* 1. Packs the tarball (npm pack --dry-run to get file list, then real pack)
|
|
6
|
+
* 2. Installs it in a fresh temp directory (no pre-existing node_modules)
|
|
7
|
+
* 3. Runs `szkrabok --version` from that install
|
|
8
|
+
* 4. Runs `szkrabok doctor` from that install
|
|
9
|
+
* 5. Cleans up and exits non-zero on any failure
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
13
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
14
|
+
import { join, resolve, dirname } from 'node:path';
|
|
15
|
+
import { tmpdir, homedir } from 'node:os';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const run = (cmd, opts = {}) => execSync(cmd, { cwd: root, stdio: 'inherit', ...opts });
|
|
20
|
+
const runCapture = (cmd, opts = {}) =>
|
|
21
|
+
execSync(cmd, { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], ...opts });
|
|
22
|
+
|
|
23
|
+
let tmpDir;
|
|
24
|
+
|
|
25
|
+
const cleanup = () => {
|
|
26
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
27
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
process.on('exit', cleanup);
|
|
32
|
+
process.on('SIGINT', () => process.exit(1));
|
|
33
|
+
|
|
34
|
+
console.log('[smoke-test] Packing tarball...');
|
|
35
|
+
const packOutput = runCapture('npm pack --json', { env: { ...process.env, npm_config_ignore_scripts: 'true' } });
|
|
36
|
+
const packInfo = JSON.parse(packOutput);
|
|
37
|
+
const tarball = resolve(root, packInfo[0].filename);
|
|
38
|
+
console.log(`[smoke-test] Packed: ${tarball}`);
|
|
39
|
+
|
|
40
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'szkrabok-smoke-'));
|
|
41
|
+
console.log(`[smoke-test] Installing into ${tmpDir}...`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
execSync(`npm install --ignore-scripts ${tarball}`, {
|
|
45
|
+
cwd: tmpDir,
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
env: { ...process.env, SZKRABOK_SKIP_BROWSER_INSTALL: '1' },
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('[smoke-test] FAIL: npm install failed');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Now run postinstall scripts manually so we control env
|
|
55
|
+
const pkgBin = join(tmpDir, 'node_modules', '.bin', 'szkrabok');
|
|
56
|
+
if (!existsSync(pkgBin)) {
|
|
57
|
+
console.error(`[smoke-test] FAIL: binary not found at ${pkgBin}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// patch-playwright (skip browser install)
|
|
62
|
+
console.log('[smoke-test] Running patch-playwright...');
|
|
63
|
+
const patchResult = spawnSync(
|
|
64
|
+
'node',
|
|
65
|
+
[join(tmpDir, 'node_modules', '@pablovitasso', 'szkrabok', 'scripts', 'patch-playwright.js')],
|
|
66
|
+
{ cwd: tmpDir, stdio: 'inherit' }
|
|
67
|
+
);
|
|
68
|
+
if (patchResult.status !== 0) {
|
|
69
|
+
console.error('[smoke-test] FAIL: patch-playwright.js failed');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --version
|
|
74
|
+
console.log('[smoke-test] Running szkrabok --version...');
|
|
75
|
+
const versionResult = spawnSync(pkgBin, ['--version'], {
|
|
76
|
+
cwd: tmpDir,
|
|
77
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
78
|
+
env: { ...process.env, SZKRABOK_SKIP_BROWSER_INSTALL: '1' },
|
|
79
|
+
});
|
|
80
|
+
if (versionResult.status !== 0) {
|
|
81
|
+
console.error('[smoke-test] FAIL: szkrabok --version failed');
|
|
82
|
+
console.error(versionResult.stderr?.toString());
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
console.log(`[smoke-test] version output: ${versionResult.stdout.toString().trim()}`);
|
|
86
|
+
|
|
87
|
+
// doctor
|
|
88
|
+
console.log('[smoke-test] Running szkrabok doctor...');
|
|
89
|
+
const doctorResult = spawnSync(pkgBin, ['doctor'], {
|
|
90
|
+
cwd: tmpDir,
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
env: { ...process.env, SZKRABOK_SKIP_BROWSER_INSTALL: '1' },
|
|
93
|
+
});
|
|
94
|
+
if (doctorResult.status !== 0) {
|
|
95
|
+
console.error('[smoke-test] FAIL: szkrabok doctor reported failures');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// remove the tarball
|
|
100
|
+
try { rmSync(tarball); } catch {}
|
|
101
|
+
|
|
102
|
+
console.log('\n[smoke-test] PASS: package installs and starts correctly.');
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function register(program, { safe, getRuntime }) {
|
|
2
|
+
program
|
|
3
|
+
.command('detect-browser')
|
|
4
|
+
.description('Detect Chrome/Chromium')
|
|
5
|
+
.action(
|
|
6
|
+
safe(async () => {
|
|
7
|
+
const { findChromiumPath } = await getRuntime();
|
|
8
|
+
const chromiumPath = await findChromiumPath();
|
|
9
|
+
|
|
10
|
+
if (!chromiumPath) {
|
|
11
|
+
console.log('No Chromium detected\n');
|
|
12
|
+
console.log(' szkrabok install-browser');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(chromiumPath);
|
|
17
|
+
console.log('\nRecommended config:\n');
|
|
18
|
+
console.log('[default]');
|
|
19
|
+
console.log(`executablePath = "${chromiumPath}"`);
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, dirname } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { resolvePlaywrightCore } from '../../../scripts/resolve-playwright-core.js';
|
|
7
|
+
|
|
8
|
+
const pass = (label, detail = '') =>
|
|
9
|
+
console.log(` [pass] ${label}${detail ? ': ' + detail : ''}`);
|
|
10
|
+
|
|
11
|
+
const fail = (label, detail = '') => {
|
|
12
|
+
console.error(` [FAIL] ${label}${detail ? ': ' + detail : ''}`);
|
|
13
|
+
return true; // signals failure
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const warn = (label, detail = '') =>
|
|
17
|
+
console.log(` [warn] ${label}${detail ? ': ' + detail : ''}`);
|
|
18
|
+
|
|
19
|
+
export function register(program) {
|
|
20
|
+
program
|
|
21
|
+
.command('doctor')
|
|
22
|
+
.description('Check szkrabok environment and dependencies')
|
|
23
|
+
.action(async () => {
|
|
24
|
+
let failed = false;
|
|
25
|
+
console.log('szkrabok doctor\n');
|
|
26
|
+
|
|
27
|
+
// 1. Node version
|
|
28
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
29
|
+
if (major >= 20) pass('node version', process.versions.node);
|
|
30
|
+
else failed = fail('node version', `${process.versions.node} (need >=20)`);
|
|
31
|
+
|
|
32
|
+
// 2. playwright-core installed — hoisting-aware resolution
|
|
33
|
+
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
34
|
+
const pwCorePath = resolvePlaywrightCore(pkgRoot, existsSync, { join });
|
|
35
|
+
if (pwCorePath) {
|
|
36
|
+
const { version } = JSON.parse(await fs.readFile(join(pwCorePath, 'package.json'), 'utf8'));
|
|
37
|
+
pass('playwright-core installed', version);
|
|
38
|
+
|
|
39
|
+
// 3. playwright-core patched
|
|
40
|
+
const crConnectionJs = join(pwCorePath, 'lib', 'server', 'chromium', 'crConnection.js');
|
|
41
|
+
if (existsSync(crConnectionJs)) {
|
|
42
|
+
const src = await fs.readFile(crConnectionJs, 'utf8');
|
|
43
|
+
if (src.includes('__re__emitExecutionContext')) pass('playwright-core patched (stealth)');
|
|
44
|
+
else warn('playwright-core not patched', 'run: node scripts/patch-playwright.js');
|
|
45
|
+
} else {
|
|
46
|
+
warn('playwright-core patch check skipped', 'crConnection.js not found');
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
failed = fail('playwright-core installed', `not found near ${pkgRoot}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 4. Chromium available
|
|
53
|
+
const playwrightCache = join(homedir(), '.cache', 'ms-playwright');
|
|
54
|
+
let chromiumFound = null;
|
|
55
|
+
if (existsSync(playwrightCache)) {
|
|
56
|
+
const dirs = readdirSync(playwrightCache)
|
|
57
|
+
.filter(d => d.startsWith('chromium-'))
|
|
58
|
+
.sort()
|
|
59
|
+
.reverse();
|
|
60
|
+
outer: for (const dir of dirs) {
|
|
61
|
+
for (const bin of ['chrome-linux/chrome', 'chrome-linux64/chrome']) {
|
|
62
|
+
const p = join(playwrightCache, dir, bin);
|
|
63
|
+
if (existsSync(p)) { chromiumFound = p; break outer; }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const p of ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome']) {
|
|
68
|
+
if (!chromiumFound && existsSync(p)) chromiumFound = p;
|
|
69
|
+
}
|
|
70
|
+
if (chromiumFound) pass('chromium', chromiumFound);
|
|
71
|
+
else failed = fail('chromium not found', 'run: szkrabok install-browser');
|
|
72
|
+
|
|
73
|
+
// 5. MCP server imports
|
|
74
|
+
try {
|
|
75
|
+
await import('../../server.js');
|
|
76
|
+
pass('server.js imports');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
failed = fail('server.js imports', err?.message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 6. Startup log
|
|
82
|
+
const logFile = join(homedir(), '.cache', 'szkrabok', 'startup.log');
|
|
83
|
+
if (existsSync(logFile)) pass('startup log exists', logFile);
|
|
84
|
+
else pass('startup log', `will be created at ${logFile}`);
|
|
85
|
+
|
|
86
|
+
console.log(`\n${failed ? 'Some checks failed.' : 'All checks passed.'}`);
|
|
87
|
+
process.exit(failed ? 1 : 0);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { endpoint } from '../../tools/szkrabok_session.js';
|
|
2
|
+
|
|
3
|
+
export function register(program, { safe }) {
|
|
4
|
+
program
|
|
5
|
+
.command('endpoint <sessionName>')
|
|
6
|
+
.description('Print CDP and WS endpoints')
|
|
7
|
+
.action(
|
|
8
|
+
safe(async sessionName => {
|
|
9
|
+
const result = await endpoint({ sessionName });
|
|
10
|
+
console.log(`CDP: ${result.cdpEndpoint}`);
|
|
11
|
+
if (result.wsEndpoint) console.log(`WS: ${result.wsEndpoint}`);
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function register(program, { safe }) {
|
|
2
|
+
program
|
|
3
|
+
.command('init')
|
|
4
|
+
.description('Scaffold minimal config')
|
|
5
|
+
.action(
|
|
6
|
+
safe(async () => {
|
|
7
|
+
const { init } = await import('../../tools/scaffold.js');
|
|
8
|
+
|
|
9
|
+
const result = await init({
|
|
10
|
+
dir: process.cwd(),
|
|
11
|
+
preset: 'minimal',
|
|
12
|
+
install: false,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (result.created.length) console.error(`Created: ${result.created.join(', ')}`);
|
|
16
|
+
if (result.merged.length) console.error(`Merged: ${result.merged.join(', ')}`);
|
|
17
|
+
if (result.skipped.length) console.error(`Skipped: ${result.skipped.join(', ')}`);
|
|
18
|
+
for (const w of result.warnings) console.error(`Warning: ${w}`);
|
|
19
|
+
|
|
20
|
+
console.error('Done. Run "szkrabok install-browser" if Chromium is not installed.');
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function register(program) {
|
|
2
|
+
program
|
|
3
|
+
.command('install-browser')
|
|
4
|
+
.description('Install Chromium via Playwright')
|
|
5
|
+
.action(() => {
|
|
6
|
+
import('node:child_process').then(({ spawn }) => {
|
|
7
|
+
const proc = spawn('npx', ['playwright', 'install', 'chromium'], { stdio: 'inherit' });
|
|
8
|
+
proc.on('close', code => process.exit(code ?? 0));
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function register(program, { safe, getRuntime, attachShutdown }) {
|
|
2
|
+
program
|
|
3
|
+
.command('open <profile>')
|
|
4
|
+
.description('Launch persistent browser and print CDP endpoint')
|
|
5
|
+
.option('--preset <preset>')
|
|
6
|
+
.option('--headless')
|
|
7
|
+
.action(
|
|
8
|
+
safe(async (profile, options) => {
|
|
9
|
+
const { launch } = await getRuntime();
|
|
10
|
+
|
|
11
|
+
const handle = await launch({
|
|
12
|
+
profile,
|
|
13
|
+
preset: options.preset,
|
|
14
|
+
headless: options.headless ?? undefined,
|
|
15
|
+
reuse: false,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
console.log(handle.cdpEndpoint);
|
|
19
|
+
|
|
20
|
+
attachShutdown(handle);
|
|
21
|
+
|
|
22
|
+
await new Promise(() => {});
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { list, deleteSession } from '../../tools/szkrabok_session.js';
|
|
4
|
+
|
|
5
|
+
const SESSIONS_DIR = path.join(process.cwd(), 'sessions');
|
|
6
|
+
|
|
7
|
+
const readJson = async file => {
|
|
8
|
+
try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch { return null; }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function register(program, { safe }) {
|
|
12
|
+
const session = program.command('session').description('Session management');
|
|
13
|
+
|
|
14
|
+
session
|
|
15
|
+
.command('list')
|
|
16
|
+
.action(
|
|
17
|
+
safe(async () => {
|
|
18
|
+
const { sessions } = await list();
|
|
19
|
+
console.table(
|
|
20
|
+
sessions.map(s => ({
|
|
21
|
+
ID: s.id,
|
|
22
|
+
Active: s.active ? 'yes' : 'no',
|
|
23
|
+
Preset: s.preset ?? 'N/A',
|
|
24
|
+
Label: s.label ?? 'N/A',
|
|
25
|
+
}))
|
|
26
|
+
);
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
session
|
|
31
|
+
.command('inspect <id>')
|
|
32
|
+
.action(
|
|
33
|
+
safe(async id => {
|
|
34
|
+
const dir = path.join(SESSIONS_DIR, id);
|
|
35
|
+
const [meta, state] = await Promise.all([
|
|
36
|
+
readJson(path.join(dir, 'meta.json')),
|
|
37
|
+
readJson(path.join(dir, 'state.json')),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
if (!meta || !state) throw new Error(`Session ${id} not found`);
|
|
41
|
+
|
|
42
|
+
console.log('=== METADATA ===');
|
|
43
|
+
console.log(JSON.stringify(meta, null, 2));
|
|
44
|
+
console.log('\n=== COOKIES ===');
|
|
45
|
+
console.log(state.cookies?.length ?? 0, 'cookies');
|
|
46
|
+
console.log('\n=== LOCALSTORAGE ===');
|
|
47
|
+
for (const origin of state.origins ?? []) {
|
|
48
|
+
console.log(origin.origin, ':', origin.localStorage?.length ?? 0, 'items');
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
session
|
|
54
|
+
.command('delete <id>')
|
|
55
|
+
.action(
|
|
56
|
+
safe(async id => {
|
|
57
|
+
await deleteSession({ sessionName: id });
|
|
58
|
+
console.log(`Session ${id} deleted`);
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
session
|
|
63
|
+
.command('cleanup')
|
|
64
|
+
.option('--days <days>', 'delete sessions older than N days', '30')
|
|
65
|
+
.action(
|
|
66
|
+
safe(async options => {
|
|
67
|
+
const days = Number(options.days) || 30;
|
|
68
|
+
const cutoff = Date.now() - days * 86400000;
|
|
69
|
+
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await fs.readdir(SESSIONS_DIR, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
console.log('No sessions directory');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await Promise.all(
|
|
79
|
+
entries
|
|
80
|
+
.filter(e => e.isDirectory())
|
|
81
|
+
.map(async e => {
|
|
82
|
+
const meta = await readJson(path.join(SESSIONS_DIR, e.name, 'meta.json'));
|
|
83
|
+
if (meta?.lastUsed && meta.lastUsed < cutoff) {
|
|
84
|
+
await deleteSession({ sessionName: e.name });
|
|
85
|
+
console.log(`Deleted: ${e.name}`);
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { program } from 'commander';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { register as registerInit } from './commands/init.js';
|
|
7
|
+
import { register as registerSession } from './commands/session.js';
|
|
8
|
+
import { register as registerOpen } from './commands/open.js';
|
|
9
|
+
import { register as registerEndpoint } from './commands/endpoint.js';
|
|
10
|
+
import { register as registerDetectBrowser } from './commands/detect-browser.js';
|
|
11
|
+
import { register as registerInstallBrowser } from './commands/install-browser.js';
|
|
12
|
+
import { register as registerDoctor } from './commands/doctor.js';
|
|
13
|
+
|
|
14
|
+
/* ---------- shared helpers ---------- */
|
|
15
|
+
|
|
16
|
+
const safe = fn => async (...args) => {
|
|
17
|
+
try {
|
|
18
|
+
await fn(...args);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error(err?.message ?? err);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const attachShutdown = handle => {
|
|
26
|
+
let closing = false;
|
|
27
|
+
const shutdown = async () => {
|
|
28
|
+
if (closing) return;
|
|
29
|
+
closing = true;
|
|
30
|
+
try { await handle.close(); } finally { process.exit(0); }
|
|
31
|
+
};
|
|
32
|
+
process.once('SIGINT', shutdown);
|
|
33
|
+
process.once('SIGTERM', shutdown);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let _runtime;
|
|
37
|
+
const getRuntime = async () => {
|
|
38
|
+
if (!_runtime) _runtime = await import('#runtime');
|
|
39
|
+
return _runtime;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/* ---------- version from package.json ---------- */
|
|
43
|
+
|
|
44
|
+
const _require = createRequire(import.meta.url);
|
|
45
|
+
const { version } = _require(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'));
|
|
46
|
+
|
|
47
|
+
/* ---------- program ---------- */
|
|
48
|
+
|
|
49
|
+
program.name('szkrabok').description('szkrabok CLI').version(version);
|
|
50
|
+
|
|
51
|
+
const ctx = { safe, attachShutdown, getRuntime };
|
|
52
|
+
|
|
53
|
+
registerInit(program, ctx);
|
|
54
|
+
registerSession(program, ctx);
|
|
55
|
+
registerOpen(program, ctx);
|
|
56
|
+
registerEndpoint(program, ctx);
|
|
57
|
+
registerDetectBrowser(program, ctx);
|
|
58
|
+
registerInstallBrowser(program, ctx);
|
|
59
|
+
registerDoctor(program, ctx);
|
|
60
|
+
|
|
61
|
+
export async function runCli() {
|
|
62
|
+
await program.parseAsync(process.argv);
|
|
63
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
|
+
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
3
6
|
|
|
4
|
-
const CLI_COMMANDS = new Set(['session', 'open', 'endpoint', 'detect-browser', 'install-browser', 'init']);
|
|
7
|
+
const CLI_COMMANDS = new Set(['session', 'open', 'endpoint', 'detect-browser', 'install-browser', 'init', 'doctor']);
|
|
5
8
|
const args = process.argv.slice(2);
|
|
6
9
|
const firstArg = args[0];
|
|
7
10
|
|
|
8
11
|
// --- CLI mode ---
|
|
9
12
|
if (firstArg && (CLI_COMMANDS.has(firstArg) || firstArg === '--setup' || firstArg === '--help' || firstArg === '-h' || firstArg === '--version' || firstArg === '-V')) {
|
|
10
|
-
const { runCli } = await import('./cli.js');
|
|
13
|
+
const { runCli } = await import('./cli/index.js');
|
|
11
14
|
await runCli();
|
|
12
15
|
process.exit(0);
|
|
13
16
|
}
|
|
@@ -17,9 +20,21 @@ if (args.includes('--no-headless') || args.includes('--headful')) {
|
|
|
17
20
|
process.env.HEADLESS = 'false';
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
// Always write fatal startup errors to a fixed log so they survive MCP client restarts.
|
|
24
|
+
const _logDir = join(homedir(), '.cache', 'szkrabok');
|
|
25
|
+
const _logFile = join(_logDir, 'startup.log');
|
|
26
|
+
const _writeStartupLog = msg => {
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(_logDir, { recursive: true });
|
|
29
|
+
appendFileSync(_logFile, `[${new Date().toISOString()}] ${msg}\n`);
|
|
30
|
+
} catch {}
|
|
31
|
+
};
|
|
32
|
+
|
|
20
33
|
const { createServer } = await import('./server.js');
|
|
21
34
|
const { log, logError } = await import('./utils/logger.js');
|
|
22
35
|
|
|
36
|
+
_writeStartupLog(`starting szkrabok pid=${process.pid}`);
|
|
37
|
+
|
|
23
38
|
const server = createServer();
|
|
24
39
|
|
|
25
40
|
process.on('SIGINT', async () => {
|
|
@@ -29,11 +44,15 @@ process.on('SIGINT', async () => {
|
|
|
29
44
|
});
|
|
30
45
|
|
|
31
46
|
process.on('uncaughtException', err => {
|
|
47
|
+
const msg = `uncaughtException: ${err?.message}\n${err?.stack}`;
|
|
48
|
+
_writeStartupLog(msg);
|
|
32
49
|
logError('Uncaught exception', err);
|
|
33
50
|
process.exit(1);
|
|
34
51
|
});
|
|
35
52
|
|
|
36
53
|
server.connect().catch(err => {
|
|
54
|
+
const msg = `Failed to start server: ${err?.message}\n${err?.stack}`;
|
|
55
|
+
_writeStartupLog(msg);
|
|
37
56
|
logError('Failed to start server', err);
|
|
38
57
|
process.exit(1);
|
|
39
58
|
});
|
package/src/cli.js
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import { program } from 'commander';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
|
|
5
|
-
import { list, deleteSession, endpoint } from './tools/szkrabok_session.js';
|
|
6
|
-
|
|
7
|
-
const SESSIONS_DIR = path.join(process.cwd(), 'sessions');
|
|
8
|
-
|
|
9
|
-
/* ---------- lazy runtime ---------- */
|
|
10
|
-
|
|
11
|
-
let runtime;
|
|
12
|
-
const getRuntime = async () => {
|
|
13
|
-
if (!runtime) runtime = await import('#runtime');
|
|
14
|
-
return runtime;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/* ---------- helpers ---------- */
|
|
18
|
-
|
|
19
|
-
const readJson = async file => {
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const safe = fn => async (...args) => {
|
|
28
|
-
try {
|
|
29
|
-
await fn(...args);
|
|
30
|
-
} catch (err) {
|
|
31
|
-
console.error(err?.message ?? err);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const attachShutdown = handle => {
|
|
37
|
-
let closing = false;
|
|
38
|
-
|
|
39
|
-
const shutdown = async () => {
|
|
40
|
-
if (closing) return;
|
|
41
|
-
closing = true;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
await handle.close();
|
|
45
|
-
} finally {
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
process.once('SIGINT', shutdown);
|
|
51
|
-
process.once('SIGTERM', shutdown);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/* ---------- program ---------- */
|
|
55
|
-
|
|
56
|
-
program
|
|
57
|
-
.name('szkrabok')
|
|
58
|
-
.description('szkrabok CLI')
|
|
59
|
-
.version('1.0.16');
|
|
60
|
-
|
|
61
|
-
/* ---------- init ---------- */
|
|
62
|
-
|
|
63
|
-
program
|
|
64
|
-
.command('init')
|
|
65
|
-
.description('Scaffold minimal config')
|
|
66
|
-
.action(
|
|
67
|
-
safe(async () => {
|
|
68
|
-
const { init } = await import('./tools/scaffold.js');
|
|
69
|
-
|
|
70
|
-
const result = await init({
|
|
71
|
-
dir: process.cwd(),
|
|
72
|
-
preset: 'minimal',
|
|
73
|
-
install: false,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (result.created.length)
|
|
77
|
-
console.error(`Created: ${result.created.join(', ')}`);
|
|
78
|
-
|
|
79
|
-
if (result.merged.length)
|
|
80
|
-
console.error(`Merged: ${result.merged.join(', ')}`);
|
|
81
|
-
|
|
82
|
-
if (result.skipped.length)
|
|
83
|
-
console.error(`Skipped: ${result.skipped.join(', ')}`);
|
|
84
|
-
|
|
85
|
-
for (const w of result.warnings)
|
|
86
|
-
console.error(`Warning: ${w}`);
|
|
87
|
-
|
|
88
|
-
console.error(
|
|
89
|
-
'Done. Run "szkrabok install-browser" if Chromium is not installed.'
|
|
90
|
-
);
|
|
91
|
-
})
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
/* ---------- session ---------- */
|
|
95
|
-
|
|
96
|
-
const session = program.command('session').description('Session management');
|
|
97
|
-
|
|
98
|
-
session
|
|
99
|
-
.command('list')
|
|
100
|
-
.action(
|
|
101
|
-
safe(async () => {
|
|
102
|
-
const { sessions } = await list();
|
|
103
|
-
|
|
104
|
-
console.table(
|
|
105
|
-
sessions.map(s => ({
|
|
106
|
-
ID: s.id,
|
|
107
|
-
Active: s.active ? 'yes' : 'no',
|
|
108
|
-
Preset: s.preset ?? 'N/A',
|
|
109
|
-
Label: s.label ?? 'N/A',
|
|
110
|
-
}))
|
|
111
|
-
);
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
session
|
|
116
|
-
.command('inspect <id>')
|
|
117
|
-
.action(
|
|
118
|
-
safe(async id => {
|
|
119
|
-
const dir = path.join(SESSIONS_DIR, id);
|
|
120
|
-
|
|
121
|
-
const [meta, state] = await Promise.all([
|
|
122
|
-
readJson(path.join(dir, 'meta.json')),
|
|
123
|
-
readJson(path.join(dir, 'state.json')),
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
if (!meta || !state)
|
|
127
|
-
throw new Error(`Session ${id} not found`);
|
|
128
|
-
|
|
129
|
-
console.log('=== METADATA ===');
|
|
130
|
-
console.log(JSON.stringify(meta, null, 2));
|
|
131
|
-
|
|
132
|
-
console.log('\n=== COOKIES ===');
|
|
133
|
-
console.log(state.cookies?.length ?? 0, 'cookies');
|
|
134
|
-
|
|
135
|
-
console.log('\n=== LOCALSTORAGE ===');
|
|
136
|
-
|
|
137
|
-
for (const origin of state.origins ?? []) {
|
|
138
|
-
console.log(
|
|
139
|
-
origin.origin,
|
|
140
|
-
':',
|
|
141
|
-
origin.localStorage?.length ?? 0,
|
|
142
|
-
'items'
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
session
|
|
149
|
-
.command('delete <id>')
|
|
150
|
-
.action(
|
|
151
|
-
safe(async id => {
|
|
152
|
-
await deleteSession({ sessionName: id });
|
|
153
|
-
console.log(`Session ${id} deleted`);
|
|
154
|
-
})
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
session
|
|
158
|
-
.command('cleanup')
|
|
159
|
-
.option('--days <days>', 'delete sessions older than N days', '30')
|
|
160
|
-
.action(
|
|
161
|
-
safe(async options => {
|
|
162
|
-
const days = Number(options.days) || 30;
|
|
163
|
-
const cutoff = Date.now() - days * 86400000;
|
|
164
|
-
|
|
165
|
-
let entries;
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
entries = await fs.readdir(SESSIONS_DIR, { withFileTypes: true });
|
|
169
|
-
} catch {
|
|
170
|
-
console.log('No sessions directory');
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
await Promise.all(
|
|
175
|
-
entries
|
|
176
|
-
.filter(e => e.isDirectory())
|
|
177
|
-
.map(async e => {
|
|
178
|
-
const meta = await readJson(
|
|
179
|
-
path.join(SESSIONS_DIR, e.name, 'meta.json')
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
if (meta?.lastUsed && meta.lastUsed < cutoff) {
|
|
183
|
-
await deleteSession({ sessionName: e.name });
|
|
184
|
-
console.log(`Deleted: ${e.name}`);
|
|
185
|
-
}
|
|
186
|
-
})
|
|
187
|
-
);
|
|
188
|
-
})
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
/* ---------- open ---------- */
|
|
192
|
-
|
|
193
|
-
program
|
|
194
|
-
.command('open <profile>')
|
|
195
|
-
.description('Launch persistent browser and print CDP endpoint')
|
|
196
|
-
.option('--preset <preset>')
|
|
197
|
-
.option('--headless')
|
|
198
|
-
.action(
|
|
199
|
-
safe(async (profile, options) => {
|
|
200
|
-
const { launch } = await getRuntime();
|
|
201
|
-
|
|
202
|
-
const handle = await launch({
|
|
203
|
-
profile,
|
|
204
|
-
preset: options.preset,
|
|
205
|
-
headless: options.headless ?? undefined,
|
|
206
|
-
reuse: false,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
console.log(handle.cdpEndpoint);
|
|
210
|
-
|
|
211
|
-
attachShutdown(handle);
|
|
212
|
-
|
|
213
|
-
await new Promise(() => {});
|
|
214
|
-
})
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
/* ---------- endpoint ---------- */
|
|
218
|
-
|
|
219
|
-
program
|
|
220
|
-
.command('endpoint <sessionName>')
|
|
221
|
-
.description('Print CDP and WS endpoints')
|
|
222
|
-
.action(
|
|
223
|
-
safe(async sessionName => {
|
|
224
|
-
const result = await endpoint({ sessionName });
|
|
225
|
-
|
|
226
|
-
console.log(`CDP: ${result.cdpEndpoint}`);
|
|
227
|
-
|
|
228
|
-
if (result.wsEndpoint)
|
|
229
|
-
console.log(`WS: ${result.wsEndpoint}`);
|
|
230
|
-
})
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
/* ---------- detect browser ---------- */
|
|
234
|
-
|
|
235
|
-
program
|
|
236
|
-
.command('detect-browser')
|
|
237
|
-
.description('Detect Chrome/Chromium')
|
|
238
|
-
.action(
|
|
239
|
-
safe(async () => {
|
|
240
|
-
const { findChromiumPath } = await getRuntime();
|
|
241
|
-
|
|
242
|
-
const chromiumPath = await findChromiumPath();
|
|
243
|
-
|
|
244
|
-
if (!chromiumPath) {
|
|
245
|
-
console.log('No Chromium detected\n');
|
|
246
|
-
console.log(' szkrabok install-browser');
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
console.log(chromiumPath);
|
|
251
|
-
|
|
252
|
-
console.log('\nRecommended config:\n');
|
|
253
|
-
console.log('[default]');
|
|
254
|
-
console.log(`executablePath = "${chromiumPath}"`);
|
|
255
|
-
})
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
/* ---------- install browser ---------- */
|
|
259
|
-
|
|
260
|
-
program
|
|
261
|
-
.command('install-browser')
|
|
262
|
-
.description('Install Chromium via Playwright')
|
|
263
|
-
.action(() => {
|
|
264
|
-
import('node:child_process').then(({ spawn }) => {
|
|
265
|
-
const proc = spawn('npx', ['playwright', 'install', 'chromium'], {
|
|
266
|
-
stdio: 'inherit',
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
proc.on('close', code => process.exit(code ?? 0));
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
/* ---------- export ---------- */
|
|
274
|
-
|
|
275
|
-
export async function runCli() {
|
|
276
|
-
await program.parseAsync(process.argv);
|
|
277
|
-
}
|