hotelzero 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,6 +81,36 @@ Logs are JSON-formatted for easy parsing:
81
81
  {"level":"info","time":"2026-02-15T12:00:02.000Z","service":"hotelzero","module":"browser","destination":"Paris","msg":"Starting hotel search"}
82
82
  ```
83
83
 
84
+ ## Session Persistence
85
+
86
+ HotelZero automatically saves browser session data (cookies, localStorage) to reduce bot detection and avoid repeated CAPTCHA challenges.
87
+
88
+ ### How It Works
89
+
90
+ - Sessions are automatically saved after each successful request
91
+ - On startup, the previous session is loaded if available
92
+ - Default session location: `~/.hotelzero/session.json`
93
+
94
+ ### Custom Session Path
95
+
96
+ Use `HOTELZERO_SESSION_PATH` to specify a custom location:
97
+
98
+ ```bash
99
+ # Custom session file location
100
+ HOTELZERO_SESSION_PATH=/path/to/session.json npx hotelzero
101
+
102
+ # Disable session persistence (use empty string)
103
+ HOTELZERO_SESSION_PATH="" npx hotelzero
104
+ ```
105
+
106
+ ### Clearing Sessions
107
+
108
+ If you experience issues, you can delete the session file:
109
+
110
+ ```bash
111
+ rm ~/.hotelzero/session.json
112
+ ```
113
+
84
114
  ## Quick Start
85
115
 
86
116
  ### Run as MCP Server
package/dist/browser.d.ts CHANGED
@@ -227,12 +227,35 @@ export interface PriceCalendarResult {
227
227
  }
228
228
  export declare class HotelBrowser {
229
229
  private browser;
230
+ private context;
230
231
  private page;
231
232
  private lastRequestTime;
232
233
  private minRequestIntervalMs;
233
234
  private proxyConfig;
234
235
  private currentUserAgent;
236
+ private sessionPath;
235
237
  init(headless?: boolean, proxy?: ProxyConfig): Promise<void>;
238
+ /**
239
+ * Save the current session (cookies, localStorage) to disk
240
+ * Call this after successful requests to persist session state
241
+ */
242
+ saveSession(): Promise<boolean>;
243
+ /**
244
+ * Check if session persistence is enabled
245
+ */
246
+ hasSessionPath(): boolean;
247
+ /**
248
+ * Get the current session file path
249
+ */
250
+ getSessionPath(): string | null;
251
+ /**
252
+ * Check if a saved session file exists
253
+ */
254
+ static hasExistingSession(): boolean;
255
+ /**
256
+ * Clear the saved session file
257
+ */
258
+ static clearSession(): boolean;
236
259
  /**
237
260
  * Check if a proxy is configured
238
261
  */
package/dist/browser.js CHANGED
@@ -1,5 +1,23 @@
1
1
  import { chromium } from "playwright";
2
2
  import { browserLogger as logger } from "./logger.js";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ // Session/cookie persistence configuration
6
+ const SESSION_PATH = process.env.HOTELZERO_SESSION_PATH || "";
7
+ /**
8
+ * Get the default session file path
9
+ * Uses ~/.hotelzero/session.json if no custom path is specified
10
+ */
11
+ function getDefaultSessionPath() {
12
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
13
+ return path.join(homeDir, ".hotelzero", "session.json");
14
+ }
15
+ /**
16
+ * Get the session file path (custom or default)
17
+ */
18
+ function getSessionPath() {
19
+ return SESSION_PATH || getDefaultSessionPath();
20
+ }
3
21
  // Custom error types for better error handling
4
22
  export class HotelSearchError extends Error {
5
23
  code;
@@ -259,17 +277,28 @@ const FILTER_CODES = {
259
277
  };
260
278
  export class HotelBrowser {
261
279
  browser = null;
280
+ context = null;
262
281
  page = null;
263
282
  lastRequestTime = 0;
264
283
  minRequestIntervalMs = 2000; // Minimum 2 seconds between requests
265
284
  proxyConfig = null;
266
285
  currentUserAgent = "";
286
+ sessionPath = "";
267
287
  async init(headless = true, proxy) {
268
288
  // Store proxy config for reference
269
289
  this.proxyConfig = proxy || null;
270
290
  // Select a random user agent for this session
271
291
  this.currentUserAgent = getRandomUserAgent();
272
- logger.debug({ headless, hasProxy: !!proxy, userAgent: this.currentUserAgent }, "Initializing browser");
292
+ // Determine session path
293
+ this.sessionPath = getSessionPath();
294
+ const hasExistingSession = this.sessionPath && fs.existsSync(this.sessionPath);
295
+ logger.debug({
296
+ headless,
297
+ hasProxy: !!proxy,
298
+ userAgent: this.currentUserAgent,
299
+ sessionPath: this.sessionPath || "(disabled)",
300
+ hasExistingSession
301
+ }, "Initializing browser");
273
302
  // Build launch options
274
303
  const launchOptions = {
275
304
  headless,
@@ -284,12 +313,90 @@ export class HotelBrowser {
284
313
  };
285
314
  }
286
315
  this.browser = await chromium.launch(launchOptions);
287
- const context = await this.browser.newContext({
316
+ // Build context options
317
+ const contextOptions = {
288
318
  userAgent: this.currentUserAgent,
289
319
  viewport: { width: 1280, height: 900 },
290
- });
291
- this.page = await context.newPage();
292
- logger.info({ hasProxy: !!proxy }, "Browser initialized successfully");
320
+ };
321
+ // Load existing session state if available
322
+ if (hasExistingSession) {
323
+ try {
324
+ contextOptions.storageState = this.sessionPath;
325
+ logger.debug({ sessionPath: this.sessionPath }, "Loading existing session");
326
+ }
327
+ catch (error) {
328
+ logger.warn({ error, sessionPath: this.sessionPath }, "Failed to load session, starting fresh");
329
+ }
330
+ }
331
+ this.context = await this.browser.newContext(contextOptions);
332
+ this.page = await this.context.newPage();
333
+ logger.info({
334
+ hasProxy: !!proxy,
335
+ sessionLoaded: hasExistingSession
336
+ }, "Browser initialized successfully");
337
+ }
338
+ /**
339
+ * Save the current session (cookies, localStorage) to disk
340
+ * Call this after successful requests to persist session state
341
+ */
342
+ async saveSession() {
343
+ if (!this.context || !this.sessionPath) {
344
+ logger.debug("Cannot save session: no context or session path disabled");
345
+ return false;
346
+ }
347
+ try {
348
+ // Ensure directory exists
349
+ const sessionDir = path.dirname(this.sessionPath);
350
+ if (!fs.existsSync(sessionDir)) {
351
+ fs.mkdirSync(sessionDir, { recursive: true });
352
+ logger.debug({ sessionDir }, "Created session directory");
353
+ }
354
+ // Save storage state (cookies + localStorage)
355
+ await this.context.storageState({ path: this.sessionPath });
356
+ logger.debug({ sessionPath: this.sessionPath }, "Session saved successfully");
357
+ return true;
358
+ }
359
+ catch (error) {
360
+ logger.warn({ error, sessionPath: this.sessionPath }, "Failed to save session");
361
+ return false;
362
+ }
363
+ }
364
+ /**
365
+ * Check if session persistence is enabled
366
+ */
367
+ hasSessionPath() {
368
+ return !!this.sessionPath;
369
+ }
370
+ /**
371
+ * Get the current session file path
372
+ */
373
+ getSessionPath() {
374
+ return this.sessionPath || null;
375
+ }
376
+ /**
377
+ * Check if a saved session file exists
378
+ */
379
+ static hasExistingSession() {
380
+ const sessionPath = getSessionPath();
381
+ return !!sessionPath && fs.existsSync(sessionPath);
382
+ }
383
+ /**
384
+ * Clear the saved session file
385
+ */
386
+ static clearSession() {
387
+ const sessionPath = getSessionPath();
388
+ if (sessionPath && fs.existsSync(sessionPath)) {
389
+ try {
390
+ fs.unlinkSync(sessionPath);
391
+ logger.info({ sessionPath }, "Session cleared");
392
+ return true;
393
+ }
394
+ catch (error) {
395
+ logger.warn({ error, sessionPath }, "Failed to clear session");
396
+ return false;
397
+ }
398
+ }
399
+ return false;
293
400
  }
294
401
  /**
295
402
  * Check if a proxy is configured
@@ -319,6 +426,7 @@ export class HotelBrowser {
319
426
  if (this.browser) {
320
427
  await this.browser.close();
321
428
  this.browser = null;
429
+ this.context = null;
322
430
  this.page = null;
323
431
  logger.debug("Browser closed");
324
432
  }
@@ -665,9 +773,13 @@ export class HotelBrowser {
665
773
  if (filters) {
666
774
  const scored = this.scoreAndFilterHotels(hotels, filters);
667
775
  logger.info({ resultCount: scored.length }, "Search completed with filters");
776
+ // Auto-save session after successful search
777
+ await this.saveSession();
668
778
  return scored;
669
779
  }
670
780
  logger.info({ resultCount: hotels.length }, "Search completed");
781
+ // Auto-save session after successful search
782
+ await this.saveSession();
671
783
  return hotels;
672
784
  }, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
673
785
  logger.warn({ attempt, error: error.message, retryInMs: delayMs }, "Search attempt failed, retrying");
@@ -1545,7 +1657,7 @@ export class HotelBrowser {
1545
1657
  else {
1546
1658
  message = `${result.roomOptions.length} room types available${lowestPrice ? ` from ${lowestPriceRoom?.priceDisplay || '$' + lowestPrice}` : ''}.`;
1547
1659
  }
1548
- return {
1660
+ const availabilityResult = {
1549
1661
  available,
1550
1662
  hotelName: result.hotelName,
1551
1663
  checkIn,
@@ -1558,6 +1670,9 @@ export class HotelBrowser {
1558
1670
  message,
1559
1671
  url: urlWithDates,
1560
1672
  };
1673
+ // Auto-save session after successful availability check
1674
+ await this.saveSession();
1675
+ return availabilityResult;
1561
1676
  }, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
1562
1677
  logger.warn({ attempt, error: error.message, retryInMs: delayMs }, "Check availability attempt failed, retrying");
1563
1678
  });
@@ -1814,7 +1929,7 @@ export class HotelBrowser {
1814
1929
  location: mainPageData.breakdown.location ?? null,
1815
1930
  freeWifi: mainPageData.breakdown.freeWifi ?? null,
1816
1931
  };
1817
- return {
1932
+ const reviewsResult = {
1818
1933
  hotelName: mainPageData.hotelName,
1819
1934
  overallRating: mainPageData.overallRating,
1820
1935
  totalReviews: mainPageData.totalReviews,
@@ -1822,6 +1937,9 @@ export class HotelBrowser {
1822
1937
  reviews: reviews.slice(0, limit),
1823
1938
  url: cleanUrl,
1824
1939
  };
1940
+ // Auto-save session after successful reviews fetch
1941
+ await this.saveSession();
1942
+ return reviewsResult;
1825
1943
  }, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
1826
1944
  logger.warn({ attempt, error: error.message, retryInMs: delayMs }, "Get reviews attempt failed, retrying");
1827
1945
  });
@@ -1991,7 +2109,7 @@ export class HotelBrowser {
1991
2109
  // Calculate end date
1992
2110
  const endDate = new Date(start);
1993
2111
  endDate.setDate(endDate.getDate() + actualNights - 1);
1994
- return {
2112
+ const priceCalendarResult = {
1995
2113
  hotelName,
1996
2114
  startDate,
1997
2115
  endDate: endDate.toISOString().split("T")[0],
@@ -2005,5 +2123,8 @@ export class HotelBrowser {
2005
2123
  averagePrice,
2006
2124
  url: cleanUrl,
2007
2125
  };
2126
+ // Auto-save session after successful price calendar fetch
2127
+ await this.saveSession();
2128
+ return priceCalendarResult;
2008
2129
  }
2009
2130
  }
package/dist/index.js CHANGED
@@ -547,7 +547,7 @@ function formatPriceCalendarResult(result) {
547
547
  // Create MCP server
548
548
  const server = new Server({
549
549
  name: "hotelzero",
550
- version: "1.11.0",
550
+ version: "1.12.0",
551
551
  }, {
552
552
  capabilities: {
553
553
  tools: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hotelzero",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "MCP server for searching hotels on Booking.com with 80+ filters",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",