@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablovitasso/szkrabok",
3
- "version": "1.0.18",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@szkrabok/runtime",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "szkrabok — browser bootstrap, stealth, session pool, MCP client.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -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 nmDir = path.resolve('node_modules')
111
+ const pkgRoot = path.resolve(__dirname, '..')
109
112
 
110
- // 1. top-level playwright-core
111
- const top = path.join(nmDir, 'playwright-core')
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
- // 2. any nested playwright-core inside other packages
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
- 'find node_modules -maxdepth 4 -name "package.json" -path "*/playwright-core/package.json" 2>/dev/null',
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 dir = path.dirname(path.resolve(line))
123
- if (!roots.includes(dir)) roots.push(dir)
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 in node_modules.')
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
+ }
@@ -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
- }