coffeeinabit 0.0.52 → 0.0.54

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.
@@ -129,10 +129,22 @@ async function tryFileBasedAutoStart(contextDir, cloudAuth, linkedinAutomation)
129
129
  */
130
130
  export async function tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation) {
131
131
  try {
132
+ const newUserEmail = sessionData.user?.email || 'default_user';
133
+
132
134
  // Check if automation is already running
133
135
  if (linkedinAutomation.isRunning) {
134
- console.log('[AutoStart] Automation is already running, skipping auto-start');
135
- return false;
136
+ // If running for the same user, skip (already running)
137
+ if (linkedinAutomation._currentUserEmail === newUserEmail) {
138
+ console.log('[AutoStart] Automation is already running for this user, skipping auto-start');
139
+ return false;
140
+ }
141
+ // If running for a different user, stop it first
142
+ console.log(`[AutoStart] Automation running for different user (${linkedinAutomation._currentUserEmail}), stopping...`);
143
+ try {
144
+ await linkedinAutomation.stopAutomation();
145
+ } catch (err) {
146
+ console.error('[AutoStart] Error stopping previous automation:', err.message);
147
+ }
136
148
  }
137
149
 
138
150
  // Create a session-like object that works with cloudAuth methods
@@ -14,6 +14,8 @@ import { executeGetMessages, closeAllConversationBubbles } from './tools/get_mes
14
14
  import { executeLinkedInSearch } from './tools/get_linkedin_search_results.js';
15
15
  import { executeGetNewMessages } from './tools/get_new_messages.js';
16
16
  import { executeGetLinkedInUpdates } from './tools/get_linkedin_updates.js';
17
+ import { executeGetNewEngagers } from './tools/get_new_engagers.js';
18
+ import { executeGetMessageThreads } from './tools/get_message_threads.js';
17
19
  import { safeGoto } from './tools/navigation.js';
18
20
  import { humanLikeAmbientMove, resetHumanMouseState } from './tools/human_mouse.js';
19
21
  import { logger } from './helpers/logger.js';
@@ -149,19 +151,24 @@ export class LinkedInAutomation {
149
151
  if (this.hasActiveBrowser()) {
150
152
  return true;
151
153
  }
152
- this.handleBrowserUnavailable(context);
154
+ // Only call handleBrowserUnavailable if not already in that state
155
+ if (this.status !== 'browser_unavailable') {
156
+ this.handleBrowserUnavailable(context);
157
+ }
153
158
  return false;
154
159
  }
155
160
 
156
161
  handleBrowserUnavailable(context) {
162
+ // Only log and stop once - avoid spam when multiple intervals detect browser gone
163
+ if (this.status === 'browser_unavailable') {
164
+ return; // Already handled
165
+ }
157
166
  logger.warn(`[LinkedInAutomation] Browser unavailable during ${context}. Stopping background activities...`);
167
+ this.status = 'browser_unavailable';
158
168
  this.stopActionPolling();
159
169
  this.stopScreenshotCapture();
160
170
  this.stopAmbientMouseMovements();
161
- if (this.status !== 'browser_unavailable') {
162
- this.status = 'browser_unavailable';
163
- this.emitStatus();
164
- }
171
+ this.emitStatus();
165
172
  }
166
173
 
167
174
  getLinkedInSessionPath() {
@@ -298,8 +305,17 @@ export class LinkedInAutomation {
298
305
 
299
306
  async startAutomation(headless = true) {
300
307
  try {
308
+ // Don't restart if already running or in authenticating phase (user trying to login)
309
+ if (this.hasActiveBrowser() &&
310
+ (this.currentPhase === this.PHASE_AUTHENTICATING ||
311
+ this.currentPhase === this.PHASE_AUTHENTICATED ||
312
+ this.currentPhase === this.PHASE_RUNNING)) {
313
+ logger.log(`[LinkedInAutomation] Browser already active in phase ${this.currentPhase}, skipping restart`);
314
+ return;
315
+ }
316
+
301
317
  logger.log('[LinkedInAutomation] Starting automation...');
302
-
318
+
303
319
  // Reset authentication promise
304
320
  this.authenticationPromise = new Promise((resolve, reject) => {
305
321
  this.authenticationResolve = resolve;
@@ -397,9 +413,9 @@ export class LinkedInAutomation {
397
413
  this.needsManualLogin = true;
398
414
  this.status = 'manual_login_needed';
399
415
  this.emitStatus();
400
-
416
+
401
417
  await this.closeBrowser();
402
-
418
+
403
419
  this.headless = false;
404
420
  await this.launchBrowserAndLogin();
405
421
 
@@ -448,8 +464,9 @@ export class LinkedInAutomation {
448
464
 
449
465
  const pollInterval = setInterval(async () => {
450
466
  try {
451
- if (!this.page || !this.isRunning) {
452
- logger.log('[LinkedInAutomation] Login polling stopped - page:', !!this.page, 'isRunning:', this.isRunning);
467
+ // Only check if page exists, not isRunning (since isRunning is false during authentication)
468
+ if (!this.page) {
469
+ logger.log('[LinkedInAutomation] Login polling stopped - no page available');
453
470
  clearInterval(pollInterval);
454
471
  return;
455
472
  }
@@ -474,28 +491,22 @@ export class LinkedInAutomation {
474
491
  const secondCheck = this.page.url();
475
492
  if (secondCheck.includes('/feed')) {
476
493
  logger.log('[LinkedInAutomation] Second check confirmed - still on /feed/');
477
-
494
+
478
495
  logger.log('[LinkedInAutomation] Session will be persisted automatically by launchPersistentContext...');
479
-
496
+
497
+ // Also save cookies explicitly for redundancy
498
+ logger.log('[LinkedInAutomation] Saving cookies explicitly after successful login...');
499
+ await this.saveLinkedInSession();
500
+
480
501
  this.status = 'linkedin_logged_in';
481
502
  this.emitStatus();
482
503
 
483
504
  if (this.originalHeadless && this.needsManualLogin) {
484
- logger.log('[LinkedInAutomation] Manual login completed. Waiting for session to be saved...');
505
+ logger.log('[LinkedInAutomation] Manual login completed. Waiting for session to persist...');
485
506
  clearInterval(pollInterval);
486
-
507
+
487
508
  await this.waitRandom(5000, 7000);
488
509
 
489
- const storageStatePath = this.getStorageStatePath();
490
- try {
491
- await this.context.storageState({ path: storageStatePath });
492
- const cookies = await this.context.cookies();
493
- logger.log(`[LinkedInAutomation] Saved storage state with ${cookies.length} cookies to: ${storageStatePath}`);
494
- } catch (error) {
495
- logger.error('[LinkedInAutomation] Error saving storage state:', error.message);
496
- }
497
- await this.waitRandom(3000, 5000);
498
-
499
510
  logger.log('[LinkedInAutomation] Switching back to headless mode...');
500
511
  this.status = 'switching_to_headless';
501
512
  this.emitStatus();
@@ -547,52 +558,75 @@ export class LinkedInAutomation {
547
558
  try {
548
559
  logger.log('[LinkedInAutomation] Closing browser...');
549
560
  this.stopAmbientMouseMovements();
550
-
561
+
551
562
  if (this.page) {
552
- await this.page.close();
563
+ try {
564
+ await this.page.close();
565
+ } catch (e) {
566
+ logger.warn('[LinkedInAutomation] Page already closed:', e.message);
567
+ }
553
568
  this.page = null;
554
569
  }
555
570
 
556
571
  if (this.context) {
572
+ // Persistent context auto-saves cookies to userDataPath on close.
573
+ // No need to save a separate storage state file.
557
574
  try {
558
- const storageStatePath = this.getStorageStatePath();
559
- const cookies = await this.context.cookies();
560
- if (cookies.length > 0) {
561
- await this.context.storageState({ path: storageStatePath });
562
- logger.log(`[LinkedInAutomation] Saved storage state with ${cookies.length} cookies to: ${storageStatePath}`);
563
- }
564
575
  await this.context.close();
565
576
  await this.waitRandom(3000, 5000);
566
577
  } catch (error) {
567
- logger.error('[LinkedInAutomation] Error saving storage state before close:', error.message);
568
- try {
569
- await this.context.close();
570
- await this.waitRandom(3000, 5000);
571
- } catch (closeError) {
572
- logger.error('[LinkedInAutomation] Error closing context:', closeError.message);
573
- }
578
+ logger.error('[LinkedInAutomation] Error closing context:', error.message);
574
579
  }
575
580
  this.context = null;
576
581
  this.browser = null;
577
582
  }
578
583
 
584
+ // Clean up stale Chromium lock files from the user data directory.
585
+ // If a previous Chromium process was killed (OOM, SIGTRAP, etc.), these
586
+ // symlinks remain and prevent a new instance from launching.
587
+ this._cleanupBrowserLockFiles();
588
+
579
589
  logger.log('[LinkedInAutomation] Browser closed successfully');
580
590
  } catch (error) {
581
591
  logger.error('[LinkedInAutomation] Error closing browser:', error);
582
592
  }
583
593
  }
584
594
 
595
+ _cleanupBrowserLockFiles() {
596
+ try {
597
+ const sanitizedEmail = (this.userEmail || 'default_user').replace(/[^a-zA-Z0-9]/g, '_');
598
+ const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
599
+ const lockFiles = ['SingletonLock', 'SingletonSocket', 'SingletonCookie'];
600
+ for (const lockFile of lockFiles) {
601
+ const lockPath = path.join(userDataPath, lockFile);
602
+ if (fs.existsSync(lockPath)) {
603
+ fs.unlinkSync(lockPath);
604
+ logger.log(`[LinkedInAutomation] Removed stale lock file: ${lockFile}`);
605
+ }
606
+ }
607
+ } catch (error) {
608
+ logger.warn('[LinkedInAutomation] Error cleaning up lock files:', error.message);
609
+ }
610
+ }
611
+
585
612
  async stopAutomation() {
586
613
  try {
587
614
  logger.log('[LinkedInAutomation] Stopping automation...');
588
615
  this.transitionToPhase(this.PHASE_STOPPING);
589
616
  this.isRunning = false;
590
617
 
591
- // Reject any pending authentication promises
618
+ // Reject any pending authentication promises (defer so stop route can complete first)
592
619
  if (this.authenticationReject) {
593
- this.authenticationReject(new Error('Automation stopped'));
620
+ const reject = this.authenticationReject;
594
621
  this.authenticationResolve = null;
595
622
  this.authenticationReject = null;
623
+ setImmediate(() => {
624
+ try {
625
+ reject(new Error('Automation stopped'));
626
+ } catch (_) {
627
+ // Consumer may have already settled; ignore
628
+ }
629
+ });
596
630
  }
597
631
 
598
632
  this.stopScreenshotCapture();
@@ -701,11 +735,19 @@ export class LinkedInAutomation {
701
735
  let currentInterval = this.pollIntervalMs;
702
736
 
703
737
  const pollWithBackoff = async () => {
738
+ // Check if polling should continue
704
739
  if (!this.isActionPollingActive) {
705
740
  logger.log('[LinkedInAutomation] Action polling stopped, not scheduling next poll');
706
741
  return;
707
742
  }
708
743
 
744
+ // Check if browser is still available before polling
745
+ if (!this.hasActiveBrowser()) {
746
+ logger.log('[LinkedInAutomation] Browser unavailable, stopping action polling loop');
747
+ this.isActionPollingActive = false;
748
+ return;
749
+ }
750
+
709
751
  if (this.currentAction) {
710
752
  logger.log('[LinkedInAutomation] Action in progress, skipping polling...');
711
753
  logger.log('[LinkedInAutomation] Next poll in', Math.round(currentInterval / 1000), 'seconds');
@@ -716,6 +758,13 @@ export class LinkedInAutomation {
716
758
  // pollForActions now returns true if actions were found and processed
717
759
  const foundActions = await this.pollForActions();
718
760
 
761
+ // Re-check after potentially long-running action execution (which includes task polling)
762
+ if (!this.isActionPollingActive || !this.hasActiveBrowser()) {
763
+ logger.log('[LinkedInAutomation] Polling stopped or browser closed during action, not scheduling next poll');
764
+ this.isActionPollingActive = false;
765
+ return;
766
+ }
767
+
719
768
  if (foundActions) {
720
769
  // reset backoff when we processed work
721
770
  currentInterval = this.pollIntervalMs;
@@ -733,6 +782,9 @@ export class LinkedInAutomation {
733
782
  }
734
783
 
735
784
  stopActionPolling() {
785
+ if (!this.isActionPollingActive) {
786
+ return; // Already stopped
787
+ }
736
788
  logger.log('[LinkedInAutomation] Action polling stopped');
737
789
  this.isActionPollingActive = false;
738
790
  }
@@ -956,7 +1008,14 @@ export class LinkedInAutomation {
956
1008
  this.emitStatus();
957
1009
 
958
1010
  let result = null;
959
-
1011
+
1012
+ // Normalize: if no username in parameters, use linkedin_profile_id from action
1013
+ if (!action.parameters?.username && action.linkedin_profile_id) {
1014
+ action.parameters = action.parameters || {};
1015
+ action.parameters.username = action.linkedin_profile_id;
1016
+ logger.log('[LinkedInAutomation] Set username from linkedin_profile_id:', action.linkedin_profile_id);
1017
+ }
1018
+
960
1019
  // add esc to close any modals that might interfere
961
1020
  await this.page.keyboard.press('Escape');
962
1021
  await this.waitRandom(100, 2000);
@@ -998,6 +1057,14 @@ export class LinkedInAutomation {
998
1057
  logger.log('[LinkedInAutomation] Executing get_linkedin_updates action...');
999
1058
  result = await executeGetLinkedInUpdates(this.page, action, await this.getAccessToken());
1000
1059
  break;
1060
+ case 'get_new_engagers':
1061
+ logger.log('[LinkedInAutomation] Executing get_new_engagers action...');
1062
+ result = await executeGetNewEngagers(this.page, action);
1063
+ break;
1064
+ case 'get_message_threads':
1065
+ logger.log('[LinkedInAutomation] Executing get_message_threads action...');
1066
+ result = await executeGetMessageThreads(this.page, action);
1067
+ break;
1001
1068
  default:
1002
1069
  logger.log('[LinkedInAutomation] Unknown action type:', action.action);
1003
1070
  result = { error: `Unknown action type: ${action.action}` };
@@ -1316,14 +1383,51 @@ export class LinkedInAutomation {
1316
1383
  return path.join(this.linkedInSessionDir, `storage_state_${sanitizedEmail}.json`);
1317
1384
  }
1318
1385
 
1386
+ async clearCookies() {
1387
+ const email = this._currentUserEmail || this.userEmail || 'default_user';
1388
+ const sanitizedEmail = email.replace(/[^a-zA-Z0-9]/g, '_');
1389
+ const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
1390
+ const storageStatePath = this.getStorageStatePath();
1391
+
1392
+ // Stop automation if running
1393
+ if (this.isRunning) {
1394
+ logger.log('[LinkedInAutomation] Stopping automation before clearing cookies...');
1395
+ await this.stopAutomation();
1396
+ }
1397
+
1398
+ // Delete the persistent context user data directory
1399
+ if (fs.existsSync(userDataPath)) {
1400
+ fs.rmSync(userDataPath, { recursive: true, force: true });
1401
+ logger.log(`[LinkedInAutomation] Deleted user data directory: ${userDataPath}`);
1402
+ }
1403
+
1404
+ // Delete the storage state file if it exists
1405
+ if (fs.existsSync(storageStatePath)) {
1406
+ fs.unlinkSync(storageStatePath);
1407
+ logger.log(`[LinkedInAutomation] Deleted storage state file: ${storageStatePath}`);
1408
+ }
1409
+
1410
+ logger.log(`[LinkedInAutomation] All cookies cleared for ${email}`);
1411
+ }
1412
+
1319
1413
  async launchBrowserAndLogin() {
1320
1414
  logger.log('[LinkedInAutomation] Launching Chromium with persistent context for LinkedIn login...');
1321
1415
 
1416
+ // Close any existing browser BEFORE updating userEmail (so cookies save to correct user's file)
1417
+ if (this.context || this.browser) {
1418
+ logger.log(`[LinkedInAutomation] Existing browser detected for user ${this.userEmail}, closing before re-launch...`);
1419
+ await this.closeBrowser(); // Uses old this.userEmail for storage path
1420
+ }
1421
+
1422
+ // Now safe to update userEmail to the new user
1423
+ const previousUserEmail = this.userEmail;
1322
1424
  this.userEmail = this._currentUserEmail || 'default_user';
1425
+ if (previousUserEmail && previousUserEmail !== this.userEmail && previousUserEmail !== 'default_user') {
1426
+ logger.log(`[LinkedInAutomation] User switched: ${previousUserEmail} -> ${this.userEmail}`);
1427
+ }
1323
1428
  const sanitizedEmail = this.userEmail.replace(/[^a-zA-Z0-9]/g, '_');
1324
1429
  const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
1325
- const storageStatePath = this.getStorageStatePath();
1326
-
1430
+
1327
1431
  logger.log('[LinkedInAutomation] User data path:', userDataPath);
1328
1432
 
1329
1433
  if (!fs.existsSync(userDataPath)) {
@@ -1331,6 +1435,9 @@ export class LinkedInAutomation {
1331
1435
  logger.log('[LinkedInAutomation] Created user data directory:', userDataPath);
1332
1436
  }
1333
1437
 
1438
+ // Clean up stale lock files before launching (from previous crashes/OOM kills)
1439
+ this._cleanupBrowserLockFiles();
1440
+
1334
1441
  const launchOptions = {
1335
1442
  headless: this.headless || false,
1336
1443
  viewport: (this.headless || false) ? { width: 1920, height: 1080 } : null,
@@ -1352,21 +1459,23 @@ export class LinkedInAutomation {
1352
1459
  await this.enablePerformanceMode();
1353
1460
  await this.preparePrimaryPage();
1354
1461
 
1355
- if (fs.existsSync(storageStatePath)) {
1356
- try {
1357
- logger.log('[LinkedInAutomation] Loading cookies from storage state file:', storageStatePath);
1358
- const storageState = JSON.parse(fs.readFileSync(storageStatePath, 'utf8'));
1359
- if (storageState.cookies && storageState.cookies.length > 0) {
1360
- await this.context.addCookies(storageState.cookies);
1361
- logger.log(`[LinkedInAutomation] Added ${storageState.cookies.length} cookies from storage state file`);
1362
- }
1363
- } catch (error) {
1364
- logger.error('[LinkedInAutomation] Error loading storage state:', error.message);
1462
+ // Persistent context already manages its own cookies via userDataPath.
1463
+ // Do NOT load extra cookies from storage state file — it causes mixing/conflicts.
1464
+
1465
+ const cookiesAfterLaunch = await this.context.cookies();
1466
+ logger.log(`[LinkedInAutomation] Total ${cookiesAfterLaunch.length} cookies in persistent context`);
1467
+
1468
+ // If persistent context has no/few cookies, try loading from JSON backup
1469
+ if (cookiesAfterLaunch.length < 5) {
1470
+ logger.log('[LinkedInAutomation] Persistent context has few/no cookies, attempting to load from JSON backup...');
1471
+ const loaded = await this.loadLinkedInSession();
1472
+ if (loaded) {
1473
+ const cookiesAfterLoad = await this.context.cookies();
1474
+ logger.log(`[LinkedInAutomation] Loaded cookies from JSON backup, now have ${cookiesAfterLoad.length} cookies`);
1475
+ } else {
1476
+ logger.log('[LinkedInAutomation] No JSON backup found or failed to load');
1365
1477
  }
1366
1478
  }
1367
-
1368
- const cookiesAfterLaunch = await this.context.cookies();
1369
- logger.log(`[LinkedInAutomation] Total ${cookiesAfterLaunch.length} cookies in context`);
1370
1479
 
1371
1480
  // await this.installCursorHighlight();
1372
1481
 
@@ -1433,21 +1542,13 @@ export class LinkedInAutomation {
1433
1542
  const currentTitle = await this.page.title();
1434
1543
  const sanitizedEmail = this.userEmail.replace(/[^a-zA-Z0-9]/g, '_');
1435
1544
  const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
1436
-
1437
- const storageStatePath = this.getStorageStatePath();
1438
- try {
1439
- await this.context.storageState({ path: storageStatePath });
1440
- const cookies = await this.context.cookies();
1441
- logger.log(`[LinkedInAutomation] Saved storage state with ${cookies.length} cookies before mode switch`);
1442
- } catch (error) {
1443
- logger.error('[LinkedInAutomation] Error saving storage state:', error);
1444
- }
1445
-
1545
+
1546
+ // Persistent context auto-saves cookies to userDataPath on close.
1446
1547
  await this.context.close();
1447
1548
  await this.waitRandom(3000, 5000);
1448
-
1549
+
1449
1550
  this.headless = newHeadless;
1450
-
1551
+
1451
1552
  const launchOptions = {
1452
1553
  headless: this.headless,
1453
1554
  viewport: null,
@@ -1459,20 +1560,8 @@ export class LinkedInAutomation {
1459
1560
  '--disable-setuid-sandbox'
1460
1561
  ]
1461
1562
  };
1462
-
1563
+
1463
1564
  this.context = await chromium.launchPersistentContext(userDataPath, launchOptions);
1464
-
1465
- if (fs.existsSync(storageStatePath)) {
1466
- try {
1467
- const storageState = JSON.parse(fs.readFileSync(storageStatePath, 'utf8'));
1468
- if (storageState.cookies && storageState.cookies.length > 0) {
1469
- await this.context.addCookies(storageState.cookies);
1470
- logger.log(`[LinkedInAutomation] Added ${storageState.cookies.length} cookies from storage state for mode switch`);
1471
- }
1472
- } catch (error) {
1473
- logger.error('[LinkedInAutomation] Error loading storage state for mode switch:', error.message);
1474
- }
1475
- }
1476
1565
 
1477
1566
  this.browser = this.context;
1478
1567
 
@@ -1539,13 +1628,11 @@ export class LinkedInAutomation {
1539
1628
  verifyAttempts++;
1540
1629
  }
1541
1630
 
1542
- logger.log('[LinkedInAutomation] Session verification failed - redirected to login. Starting fresh login...');
1543
- await this.startLinkedInLogin();
1631
+ logger.log('[LinkedInAutomation] Session verification failed - redirected to login.');
1544
1632
  return false;
1545
1633
  } catch (error) {
1546
1634
  logger.error('[LinkedInAutomation] Error verifying session:', error);
1547
- logger.log('[LinkedInAutomation] Falling back to fresh login...');
1548
- await this.startLinkedInLogin();
1635
+ logger.log('[LinkedInAutomation] Session check failed, will need fresh login.');
1549
1636
  return false;
1550
1637
  }
1551
1638
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.52",
3
+ "version": "0.0.54",
4
4
  "description": "coffeeinabit app",
5
5
  "main": "server.js",
6
6
  "type": "module",