@aluvia/sdk 1.4.1 → 2.0.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +162 -477
  3. package/dist/cjs/api/apiUtils.js +4 -1
  4. package/dist/cjs/client/AluviaClient.js +30 -32
  5. package/dist/cjs/client/BlockDetection.js +69 -87
  6. package/dist/cjs/client/rules.js +12 -2
  7. package/dist/cjs/connect.js +2 -2
  8. package/dist/cjs/index.js +12 -1
  9. package/dist/cjs/session/lock.js +40 -4
  10. package/dist/esm/api/apiUtils.js +4 -1
  11. package/dist/esm/client/AluviaClient.js +38 -40
  12. package/dist/esm/client/BlockDetection.js +69 -87
  13. package/dist/esm/client/rules.js +12 -2
  14. package/dist/esm/connect.js +2 -2
  15. package/dist/esm/index.js +6 -4
  16. package/dist/esm/session/lock.js +40 -4
  17. package/dist/types/client/AluviaClient.d.ts +2 -2
  18. package/dist/types/client/BlockDetection.d.ts +4 -4
  19. package/dist/types/client/types.d.ts +11 -11
  20. package/dist/types/index.d.ts +9 -7
  21. package/package.json +15 -23
  22. package/dist/cjs/bin/account.js +0 -31
  23. package/dist/cjs/bin/api-helpers.js +0 -58
  24. package/dist/cjs/bin/cli-adapter.js +0 -16
  25. package/dist/cjs/bin/cli.js +0 -245
  26. package/dist/cjs/bin/close.js +0 -120
  27. package/dist/cjs/bin/geos.js +0 -10
  28. package/dist/cjs/bin/mcp-helpers.js +0 -57
  29. package/dist/cjs/bin/open.js +0 -317
  30. package/dist/cjs/bin/session.js +0 -259
  31. package/dist/esm/bin/account.js +0 -28
  32. package/dist/esm/bin/api-helpers.js +0 -53
  33. package/dist/esm/bin/cli-adapter.js +0 -8
  34. package/dist/esm/bin/cli.js +0 -242
  35. package/dist/esm/bin/close.js +0 -117
  36. package/dist/esm/bin/geos.js +0 -7
  37. package/dist/esm/bin/mcp-helpers.js +0 -51
  38. package/dist/esm/bin/open.js +0 -280
  39. package/dist/esm/bin/session.js +0 -252
  40. package/dist/types/bin/account.d.ts +0 -1
  41. package/dist/types/bin/api-helpers.d.ts +0 -20
  42. package/dist/types/bin/cli-adapter.d.ts +0 -8
  43. package/dist/types/bin/cli.d.ts +0 -2
  44. package/dist/types/bin/close.d.ts +0 -1
  45. package/dist/types/bin/geos.d.ts +0 -1
  46. package/dist/types/bin/mcp-helpers.d.ts +0 -28
  47. package/dist/types/bin/open.d.ts +0 -21
  48. package/dist/types/bin/session.d.ts +0 -11
@@ -1,117 +0,0 @@
1
- import { removeLock, isProcessAlive, listSessions, toLockData } from '../session/lock.js';
2
- import { output } from './cli.js';
3
- export async function handleClose(sessionName, closeAll) {
4
- if (closeAll) {
5
- const sessions = listSessions();
6
- if (sessions.length === 0) {
7
- return output({ error: 'No running browser sessions found.', closed: [], count: 0 }, 1);
8
- }
9
- // Send SIGTERM to all sessions
10
- for (const s of sessions) {
11
- try {
12
- process.kill(s.pid, 'SIGTERM');
13
- }
14
- catch {
15
- // ignore
16
- }
17
- }
18
- // Wait up to 10 seconds for all processes to exit
19
- const maxWait = 40;
20
- const alive = new Set(sessions.map((s) => s.pid));
21
- for (let i = 0; i < maxWait && alive.size > 0; i++) {
22
- await new Promise((r) => setTimeout(r, 250));
23
- for (const pid of alive) {
24
- if (!isProcessAlive(pid)) {
25
- alive.delete(pid);
26
- }
27
- }
28
- }
29
- // Force-kill any survivors
30
- for (const pid of alive) {
31
- try {
32
- process.kill(pid, 'SIGKILL');
33
- }
34
- catch {
35
- // ignore
36
- }
37
- }
38
- // Now remove all locks
39
- const closed = [];
40
- for (const s of sessions) {
41
- removeLock(s.session);
42
- closed.push(s.session);
43
- }
44
- return output({ message: 'All browser sessions closed.', closed, count: closed.length });
45
- }
46
- // If no session name specified, figure out what to close
47
- if (!sessionName) {
48
- const sessions = listSessions();
49
- if (sessions.length === 0) {
50
- return output({ error: 'No running browser sessions found.' }, 1);
51
- }
52
- if (sessions.length > 1) {
53
- return output({
54
- error: 'Multiple sessions running. Specify --browser-session <name> or --all.',
55
- browserSessions: sessions.map((s) => s.session),
56
- }, 1);
57
- }
58
- // Single session — use its data directly instead of re-reading the lock file
59
- const session = sessions[0];
60
- sessionName = session.session;
61
- return closeSession(sessionName, toLockData(session));
62
- }
63
- // Session name provided — need to verify it's alive
64
- const sessions = listSessions();
65
- const match = sessions.find((s) => s.session === sessionName);
66
- if (!match) {
67
- return output({ browserSession: sessionName, error: 'No running browser session found.' }, 1);
68
- }
69
- return closeSession(sessionName, toLockData(match));
70
- }
71
- async function closeSession(sessionName, lock) {
72
- if (!isProcessAlive(lock.pid)) {
73
- removeLock(sessionName);
74
- return output({
75
- browserSession: sessionName,
76
- message: 'Browser process was not running. Lock file cleaned up.',
77
- });
78
- }
79
- try {
80
- process.kill(lock.pid, 'SIGTERM');
81
- }
82
- catch (err) {
83
- return output({ browserSession: sessionName, error: `Failed to stop process: ${err.message}` }, 1);
84
- }
85
- // Wait for the process to exit (up to 10 seconds)
86
- const maxWait = 40;
87
- for (let i = 0; i < maxWait; i++) {
88
- await new Promise((r) => setTimeout(r, 250));
89
- if (!isProcessAlive(lock.pid)) {
90
- removeLock(sessionName);
91
- return output({
92
- browserSession: sessionName,
93
- pid: lock.pid,
94
- message: 'Browser session closed.',
95
- startUrl: lock.url ?? null,
96
- cdpUrl: lock.cdpUrl ?? null,
97
- connectionId: lock.connectionId ?? null,
98
- });
99
- }
100
- }
101
- // Force kill if still alive
102
- try {
103
- process.kill(lock.pid, 'SIGKILL');
104
- }
105
- catch {
106
- // ignore
107
- }
108
- removeLock(sessionName);
109
- return output({
110
- browserSession: sessionName,
111
- pid: lock.pid,
112
- message: 'Browser session force-killed.',
113
- startUrl: lock.url ?? null,
114
- cdpUrl: lock.cdpUrl ?? null,
115
- connectionId: lock.connectionId ?? null,
116
- });
117
- }
@@ -1,7 +0,0 @@
1
- import { requireApi } from './api-helpers.js';
2
- import { output } from './cli.js';
3
- export async function handleGeos() {
4
- const api = requireApi();
5
- const geos = await api.geos.list();
6
- return output({ geos, count: geos.length });
7
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * MCP output capture helpers.
3
- *
4
- * The CLI handlers call `output()` which normally does `console.log` + `process.exit()`.
5
- * In MCP mode, we switch `output()` to throw an MCPOutputCapture instead,
6
- * allowing us to catch and return the data without exiting the process.
7
- *
8
- * Uses AsyncLocalStorage so concurrent MCP tool calls don't interfere.
9
- */
10
- import { AsyncLocalStorage } from "node:async_hooks";
11
- /**
12
- * Thrown by output() when in capture mode.
13
- * Contains the JSON data and exit code that would have been written to stdout.
14
- */
15
- export class MCPOutputCapture {
16
- constructor(data, exitCode) {
17
- this.data = data;
18
- this.exitCode = exitCode;
19
- }
20
- }
21
- const captureContext = new AsyncLocalStorage();
22
- export function isCapturing() {
23
- return captureContext.getStore() ?? false;
24
- }
25
- /**
26
- * Run a CLI handler function in capture mode.
27
- * Returns the data that output() would have written to stdout.
28
- * Safe for concurrent use — each call gets its own async context.
29
- */
30
- export async function captureOutput(fn) {
31
- return captureContext.run(true, async () => {
32
- try {
33
- await fn();
34
- // Handler completed without calling output() — shouldn't happen for CLI handlers
35
- return { data: { error: "Handler did not produce output" }, isError: true };
36
- }
37
- catch (err) {
38
- if (err instanceof MCPOutputCapture) {
39
- return {
40
- data: err.data,
41
- isError: err.exitCode !== 0,
42
- };
43
- }
44
- // Unexpected error (not from output())
45
- return {
46
- data: { error: err instanceof Error ? err.message : String(err) },
47
- isError: true,
48
- };
49
- }
50
- });
51
- }
@@ -1,280 +0,0 @@
1
- import { AluviaClient } from '../client/AluviaClient.js';
2
- import { writeLock, readLock, removeLock, isProcessAlive, getLogFilePath, generateSessionName, validateSessionName } from '../session/lock.js';
3
- import { output } from './cli.js';
4
- import { spawn } from 'node:child_process';
5
- import * as fs from 'node:fs';
6
- import * as path from 'node:path';
7
- import { pathToFileURL, fileURLToPath } from 'node:url';
8
- // Determine the directory of this module at load time
9
- // @ts-ignore - import.meta.url exists at runtime in ESM
10
- const thisModuleDir = path.dirname(fileURLToPath(import.meta.url));
11
- /**
12
- * Get the path to cli.js for spawning daemon processes.
13
- * Looks in the same directory as this module (works for both dev and installed).
14
- */
15
- function getCliScriptPath() {
16
- // cli.js should be in the same directory as open.js
17
- const cliPath = path.join(thisModuleDir, 'cli.js');
18
- if (fs.existsSync(cliPath)) {
19
- return cliPath;
20
- }
21
- throw new Error(`Could not find cli.js at ${cliPath}`);
22
- }
23
- /**
24
- * Called from cli.ts when running `session start <url>`.
25
- * Spawns the actual browser in a detached child and polls until ready.
26
- * Returns a Promise that resolves via process.exit() (never returns normally).
27
- */
28
- export function handleOpen({ url, connectionId, headless, sessionName, autoUnblock, disableBlockDetection, run }) {
29
- // Generate session name if not provided
30
- const session = sessionName ?? generateSessionName();
31
- // Validate session name early (before spawning daemon)
32
- if (sessionName && !validateSessionName(sessionName)) {
33
- output({ error: 'Invalid session name. Use only letters, numbers, hyphens, and underscores.' }, 1);
34
- }
35
- // Check for existing instance with this session name
36
- const existing = readLock(session);
37
- if (existing !== null && isProcessAlive(existing.pid)) {
38
- output({
39
- error: `A browser session named '${session}' is already running.`,
40
- browserSession: session,
41
- startUrl: existing.url ?? null,
42
- cdpUrl: existing.cdpUrl ?? null,
43
- connectionId: existing.connectionId ?? null,
44
- pid: existing.pid,
45
- }, 1);
46
- }
47
- // Clean up stale lock if process is dead
48
- if (existing !== null) {
49
- removeLock(session);
50
- }
51
- // Require API key
52
- const apiKey = process.env.ALUVIA_API_KEY;
53
- if (!apiKey) {
54
- output({ error: 'ALUVIA_API_KEY environment variable is required.' }, 1);
55
- }
56
- // Spawn a detached child process that runs the daemon
57
- const logFile = getLogFilePath(session);
58
- const out = fs.openSync(logFile, 'a');
59
- const args = ['--daemon', url, '--browser-session', session];
60
- if (connectionId != null) {
61
- args.push('--connection-id', String(connectionId));
62
- }
63
- if (!headless) {
64
- args.push('--headful');
65
- }
66
- if (autoUnblock) {
67
- args.push('--auto-unblock');
68
- }
69
- if (disableBlockDetection) {
70
- args.push('--disable-block-detection');
71
- }
72
- if (run) {
73
- args.push('--run', run);
74
- }
75
- let child;
76
- try {
77
- // Get the path to cli.js in the same directory as this module
78
- const cliPath = getCliScriptPath();
79
- child = spawn(process.execPath, [cliPath, ...args], {
80
- detached: true,
81
- stdio: ['ignore', out, out],
82
- env: { ...process.env, ALUVIA_API_KEY: apiKey },
83
- });
84
- child.unref();
85
- }
86
- catch (err) {
87
- fs.closeSync(out);
88
- return output({ browserSession: session, error: `Failed to spawn browser process: ${err.message}` }, 1);
89
- }
90
- fs.closeSync(out);
91
- // Wait for the daemon to be fully ready (lock file with ready: true)
92
- return new Promise((resolve, reject) => {
93
- let attempts = 0;
94
- const maxAttempts = 240; // 60 seconds max
95
- const poll = setInterval(() => {
96
- attempts++;
97
- try {
98
- // Early exit if daemon process died
99
- if (child.pid && !isProcessAlive(child.pid)) {
100
- clearInterval(poll);
101
- removeLock(session);
102
- output({
103
- browserSession: session,
104
- error: 'Browser process exited unexpectedly.',
105
- logFile,
106
- }, 1);
107
- }
108
- const lock = readLock(session);
109
- if (lock && lock.ready) {
110
- clearInterval(poll);
111
- output({
112
- browserSession: session,
113
- pid: lock.pid,
114
- startUrl: lock.url ?? null,
115
- cdpUrl: lock.cdpUrl ?? null,
116
- connectionId: lock.connectionId ?? null,
117
- blockDetection: lock.blockDetection ?? false,
118
- autoUnblock: lock.autoUnblock ?? false,
119
- });
120
- }
121
- if (attempts >= maxAttempts) {
122
- clearInterval(poll);
123
- const alive = child.pid ? isProcessAlive(child.pid) : false;
124
- output({
125
- browserSession: session,
126
- error: alive ? 'Browser is still initializing (timeout).' : 'Browser process exited unexpectedly.',
127
- logFile,
128
- }, 1);
129
- }
130
- }
131
- catch (err) {
132
- // In MCP capture mode, output() throws MCPOutputCapture which we need to propagate
133
- clearInterval(poll);
134
- reject(err);
135
- }
136
- }, 250);
137
- });
138
- }
139
- /**
140
- * Daemon entry point — runs in the detached child process.
141
- * Starts the proxy + browser, writes lock, and stays alive.
142
- * Logs go to the daemon log file (stdout is redirected), not to the user.
143
- */
144
- export async function handleOpenDaemon({ url, connectionId, headless, sessionName, autoUnblock, disableBlockDetection, run }) {
145
- const apiKey = process.env.ALUVIA_API_KEY;
146
- const blockDetectionEnabled = !disableBlockDetection;
147
- const updateLockWithDetection = (result) => {
148
- const lock = readLock(sessionName);
149
- if (!lock)
150
- return;
151
- const lastDetection = {
152
- hostname: result.hostname,
153
- lastUrl: result.url,
154
- blockStatus: result.blockStatus,
155
- score: result.score,
156
- signals: result.signals.map((s) => s.name),
157
- pass: result.pass,
158
- persistentBlock: result.persistentBlock,
159
- timestamp: Date.now(),
160
- };
161
- writeLock({ ...lock, lastDetection }, sessionName);
162
- };
163
- const client = new AluviaClient({
164
- apiKey,
165
- startPlaywright: true,
166
- ...(connectionId != null ? { connectionId } : {}),
167
- headless: headless ?? true,
168
- blockDetection: blockDetectionEnabled
169
- ? autoUnblock
170
- ? { enabled: true, autoUnblock: true, onDetection: updateLockWithDetection }
171
- : { enabled: true, onDetection: updateLockWithDetection }
172
- : { enabled: false },
173
- });
174
- const connection = await client.start();
175
- // Write early lock so parent knows daemon is alive
176
- writeLock({ pid: process.pid, session: sessionName, url, proxyUrl: connection.url, blockDetection: blockDetectionEnabled, autoUnblock: blockDetectionEnabled && !!autoUnblock }, sessionName);
177
- if (autoUnblock)
178
- console.log('[daemon] Auto-unblock enabled');
179
- console.log(`[daemon] Browser initialized — proxy: ${connection.url}`);
180
- if (connection.cdpUrl)
181
- console.log(`[daemon] CDP URL: ${connection.cdpUrl}`);
182
- if (connectionId != null)
183
- console.log(`[daemon] Connection ID: ${connectionId}`);
184
- if (sessionName)
185
- console.log(`[daemon] Session: ${sessionName}`);
186
- console.log(`[daemon] Opening ${url}`);
187
- // Navigate to URL in the browser
188
- const page = await connection.browserContext.newPage();
189
- await page.goto(url, { waitUntil: 'domcontentloaded' });
190
- // Gather session info
191
- const pageTitle = await page.title().catch(() => '');
192
- const cdpUrl = connection.cdpUrl ?? '';
193
- // Get connection ID: use the one passed in, or read from ConfigManager
194
- const connId = connectionId ?? client.connectionId;
195
- // Write lock file with full session metadata (marks session as ready)
196
- // Read existing lock first to preserve lastDetection written by the onDetection callback
197
- const existingLock = readLock(sessionName);
198
- writeLock({
199
- pid: process.pid,
200
- session: sessionName,
201
- connectionId: connId,
202
- cdpUrl,
203
- proxyUrl: connection.url,
204
- url,
205
- ready: true,
206
- blockDetection: blockDetectionEnabled,
207
- autoUnblock: blockDetectionEnabled && !!autoUnblock,
208
- lastDetection: existingLock?.lastDetection,
209
- }, sessionName);
210
- console.log(`[daemon] Session ready — session: ${sessionName ?? 'default'}, url: ${url}, cdpUrl: ${cdpUrl}, connectionId: ${connId ?? 'unknown'}, pid: ${process.pid}`);
211
- if (pageTitle)
212
- console.log(`[daemon] Page title: ${pageTitle}`);
213
- // If --run was provided, execute the script and then shut down
214
- if (run) {
215
- const scriptPath = path.resolve(run);
216
- if (!fs.existsSync(scriptPath)) {
217
- console.error(`[daemon] Script not found: ${scriptPath}`);
218
- removeLock(sessionName);
219
- await connection.close();
220
- process.exit(1);
221
- }
222
- console.log(`[daemon] Running script: ${scriptPath}`);
223
- // Inject page, browser, context as globals so the script can use them directly
224
- const browser = connection.browser;
225
- const context = connection.browserContext;
226
- globalThis.page = page;
227
- globalThis.browser = browser;
228
- globalThis.context = context;
229
- let exitCode = 0;
230
- try {
231
- await import(pathToFileURL(scriptPath).href);
232
- }
233
- catch (err) {
234
- console.error(`[daemon] Script error: ${err.message}`);
235
- if (err.stack)
236
- console.error(err.stack);
237
- exitCode = 1;
238
- }
239
- // Clean up globals
240
- delete globalThis.page;
241
- delete globalThis.browser;
242
- delete globalThis.context;
243
- console.log(`[daemon] Script finished.`);
244
- removeLock(sessionName);
245
- await connection.close();
246
- process.exit(exitCode);
247
- }
248
- // Graceful shutdown handler
249
- let stopping = false;
250
- const shutdown = async () => {
251
- if (stopping)
252
- return;
253
- stopping = true;
254
- console.log('[daemon] Shutting down...');
255
- try {
256
- await connection.close();
257
- }
258
- catch {
259
- // ignore
260
- }
261
- removeLock(sessionName);
262
- console.log('[daemon] Stopped.');
263
- process.exit(0);
264
- };
265
- process.on('SIGINT', shutdown);
266
- process.on('SIGTERM', shutdown);
267
- // Detect when the browser is closed by the user
268
- if (connection.browser) {
269
- connection.browser.on('disconnected', () => {
270
- if (!stopping) {
271
- console.log('[daemon] Browser closed by user.');
272
- removeLock(sessionName);
273
- client
274
- .stop()
275
- .catch(() => { })
276
- .finally(() => process.exit(0));
277
- }
278
- });
279
- }
280
- }