linkedin-agent 1.0.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 +119 -0
- package/assets/default-logo.png +0 -0
- package/dist/auth.js +235 -0
- package/dist/browser.js +72 -0
- package/dist/cli.js +514 -0
- package/dist/dev-app.js +422 -0
- package/dist/index.js +12 -0
- package/dist/poster.js +120 -0
- package/dist/scraper.js +256 -0
- package/package.json +47 -0
- package/scripts/postinstall.js +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# linkedin-agent
|
|
2
|
+
|
|
3
|
+
LinkedIn CLI tool for scraping posts, publishing, editing, and deleting — all from your terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g linkedin-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx linkedin-agent
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `get` — Scrape posts
|
|
20
|
+
|
|
21
|
+
Collects posts and engagement metrics (likes, comments, shares) via browser automation.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Scrape your own posts
|
|
25
|
+
linkedin-agent get
|
|
26
|
+
|
|
27
|
+
# Scrape a specific profile
|
|
28
|
+
linkedin-agent get -p https://www.linkedin.com/in/someone
|
|
29
|
+
|
|
30
|
+
# Limit to recent 10 posts
|
|
31
|
+
linkedin-agent get -l 10
|
|
32
|
+
|
|
33
|
+
# Specify output directory
|
|
34
|
+
linkedin-agent get -o ./data
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
On first run, a browser window opens for LinkedIn login. The session persists across runs.
|
|
38
|
+
|
|
39
|
+
Repeated runs on the same day **merge** with existing data (no duplicates).
|
|
40
|
+
|
|
41
|
+
**Options:**
|
|
42
|
+
| Flag | Description | Default |
|
|
43
|
+
|------|-------------|---------|
|
|
44
|
+
| `-p, --profile <url>` | Target profile URL | Your profile |
|
|
45
|
+
| `-l, --limit <n>` | Max posts to collect | All |
|
|
46
|
+
| `-o, --output <dir>` | Output directory | Current directory |
|
|
47
|
+
| `-m, --max-scrolls <n>` | Max scroll iterations | 100 |
|
|
48
|
+
|
|
49
|
+
### `auth` — Set up OAuth
|
|
50
|
+
|
|
51
|
+
Required for `post`, `edit`, and `delete` commands.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Auto: creates a Developer App + OAuth in one step
|
|
55
|
+
linkedin-agent auth
|
|
56
|
+
|
|
57
|
+
# Manual: use existing app credentials
|
|
58
|
+
linkedin-agent auth --client-id YOUR_ID --client-secret YOUR_SECRET
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `post` — Publish a post
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Post inline text
|
|
65
|
+
linkedin-agent post -t "Hello LinkedIn!"
|
|
66
|
+
|
|
67
|
+
# Post from a file
|
|
68
|
+
linkedin-agent post -f ./post.md
|
|
69
|
+
|
|
70
|
+
# Post with a link attachment
|
|
71
|
+
linkedin-agent post -t "Check this out" --link https://example.com
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `edit` — Edit a post
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
linkedin-agent edit --id "urn:li:share:123456" -t "Updated content"
|
|
78
|
+
linkedin-agent edit --id "urn:li:share:123456" -f ./updated.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `delete` — Delete a post
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
linkedin-agent delete --id "urn:li:share:123456"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Output Format
|
|
88
|
+
|
|
89
|
+
`get` saves posts as JSON:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
"id": "urn:li:activity:123456",
|
|
95
|
+
"text": "Post content...",
|
|
96
|
+
"publishedAt": "2w",
|
|
97
|
+
"numLikes": 42,
|
|
98
|
+
"numComments": 5,
|
|
99
|
+
"numShares": 3,
|
|
100
|
+
"url": "https://www.linkedin.com/feed/update/urn:li:activity:123456/"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
File naming: `posts_{username}_{date}.json`
|
|
106
|
+
|
|
107
|
+
## How It Works
|
|
108
|
+
|
|
109
|
+
- **`get`**: Uses Playwright to open a real browser, intercepts LinkedIn's internal Voyager API responses as you scroll, and extracts post data.
|
|
110
|
+
- **`post/edit/delete`**: Uses LinkedIn's official REST API with OAuth 2.0 authentication.
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Node.js 18+
|
|
115
|
+
- Chromium (auto-installed via Playwright on first run)
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
ISC
|
|
Binary file
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadCredentials = loadCredentials;
|
|
37
|
+
exports.authenticate = authenticate;
|
|
38
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
39
|
+
exports.getValidCredentials = getValidCredentials;
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const http = __importStar(require("http"));
|
|
42
|
+
const https = __importStar(require("https"));
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const CREDENTIALS_PATH = path.join(os.homedir(), ".linkedin-agent", "credentials.json");
|
|
48
|
+
const REDIRECT_URI = "http://localhost:3000/callback";
|
|
49
|
+
const SCOPES = "openid profile email w_member_social";
|
|
50
|
+
function httpsPost(url, data, headers) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const urlObj = new URL(url);
|
|
53
|
+
const req = https.request({
|
|
54
|
+
hostname: urlObj.hostname,
|
|
55
|
+
path: urlObj.pathname + urlObj.search,
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { ...headers, "Content-Length": Buffer.byteLength(data) },
|
|
58
|
+
}, (res) => {
|
|
59
|
+
let body = "";
|
|
60
|
+
res.on("data", (chunk) => (body += chunk));
|
|
61
|
+
res.on("end", () => resolve({ status: res.statusCode || 0, body }));
|
|
62
|
+
});
|
|
63
|
+
req.on("error", reject);
|
|
64
|
+
req.write(data);
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function httpsGet(url, headers) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const urlObj = new URL(url);
|
|
71
|
+
const req = https.request({
|
|
72
|
+
hostname: urlObj.hostname,
|
|
73
|
+
path: urlObj.pathname + urlObj.search,
|
|
74
|
+
method: "GET",
|
|
75
|
+
headers,
|
|
76
|
+
}, (res) => {
|
|
77
|
+
let body = "";
|
|
78
|
+
res.on("data", (chunk) => (body += chunk));
|
|
79
|
+
res.on("end", () => resolve({ status: res.statusCode || 0, body }));
|
|
80
|
+
});
|
|
81
|
+
req.on("error", reject);
|
|
82
|
+
req.end();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function saveCredentials(creds) {
|
|
86
|
+
fs.mkdirSync(path.dirname(CREDENTIALS_PATH), { recursive: true, mode: 0o700 });
|
|
87
|
+
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
88
|
+
}
|
|
89
|
+
function loadCredentials() {
|
|
90
|
+
if (!fs.existsSync(CREDENTIALS_PATH))
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function authenticate(clientId, clientSecret, openUrlFn) {
|
|
100
|
+
// 1. Start callback server and wait for authorization code
|
|
101
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
102
|
+
const authCode = await new Promise((resolve, reject) => {
|
|
103
|
+
const server = http.createServer((req, res) => {
|
|
104
|
+
const url = new URL(req.url || "", "http://localhost:3000");
|
|
105
|
+
const code = url.searchParams.get("code");
|
|
106
|
+
const returnedState = url.searchParams.get("state");
|
|
107
|
+
const error = url.searchParams.get("error");
|
|
108
|
+
if (error) {
|
|
109
|
+
const desc = url.searchParams.get("error_description") || error;
|
|
110
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
111
|
+
res.end(`Error: ${desc}`);
|
|
112
|
+
reject(new Error(`OAuth error: ${desc}`));
|
|
113
|
+
server.close();
|
|
114
|
+
}
|
|
115
|
+
else if (code && returnedState === state) {
|
|
116
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
117
|
+
res.end("Authentication complete! You can close this window.");
|
|
118
|
+
resolve(code);
|
|
119
|
+
server.close();
|
|
120
|
+
}
|
|
121
|
+
else if (code) {
|
|
122
|
+
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
|
|
123
|
+
res.end("Invalid state parameter.");
|
|
124
|
+
reject(new Error("OAuth CSRF detected: state mismatch"));
|
|
125
|
+
server.close();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
res.writeHead(404);
|
|
129
|
+
res.end();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const timeout = setTimeout(() => {
|
|
133
|
+
server.close();
|
|
134
|
+
reject(new Error("OAuth callback timed out after 5 minutes"));
|
|
135
|
+
}, 5 * 60 * 1000);
|
|
136
|
+
server.on("close", () => clearTimeout(timeout));
|
|
137
|
+
server.listen(3000, "127.0.0.1", async () => {
|
|
138
|
+
const authUrl = "https://www.linkedin.com/oauth/v2/authorization?" +
|
|
139
|
+
new URLSearchParams({
|
|
140
|
+
response_type: "code",
|
|
141
|
+
client_id: clientId,
|
|
142
|
+
redirect_uri: REDIRECT_URI,
|
|
143
|
+
scope: SCOPES,
|
|
144
|
+
state,
|
|
145
|
+
}).toString();
|
|
146
|
+
console.log("Opening browser for LinkedIn authentication...");
|
|
147
|
+
console.log(`\nIf the browser doesn't open, visit this URL:\n${authUrl}\n`);
|
|
148
|
+
if (openUrlFn) {
|
|
149
|
+
await openUrlFn(authUrl);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
(0, child_process_1.execFile)("open", [authUrl]);
|
|
153
|
+
}
|
|
154
|
+
console.log("Waiting for callback on localhost:3000...");
|
|
155
|
+
});
|
|
156
|
+
server.on("error", (err) => {
|
|
157
|
+
reject(new Error(`Failed to start callback server: ${err.message}`));
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
console.log("Authorization code received.");
|
|
161
|
+
// 2. Exchange code for tokens
|
|
162
|
+
console.log("Exchanging code for tokens...");
|
|
163
|
+
const tokenResp = await httpsPost("https://www.linkedin.com/oauth/v2/accessToken", new URLSearchParams({
|
|
164
|
+
grant_type: "authorization_code",
|
|
165
|
+
code: authCode,
|
|
166
|
+
client_id: clientId,
|
|
167
|
+
client_secret: clientSecret,
|
|
168
|
+
redirect_uri: REDIRECT_URI,
|
|
169
|
+
}).toString(), { "Content-Type": "application/x-www-form-urlencoded" });
|
|
170
|
+
const tokens = JSON.parse(tokenResp.body);
|
|
171
|
+
if (!tokens.access_token) {
|
|
172
|
+
throw new Error(`Token exchange failed: ${tokenResp.body}`);
|
|
173
|
+
}
|
|
174
|
+
console.log("Access token obtained.");
|
|
175
|
+
// 3. Get person ID
|
|
176
|
+
console.log("Fetching person ID...");
|
|
177
|
+
const userResp = await httpsGet("https://api.linkedin.com/v2/userinfo", {
|
|
178
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
179
|
+
});
|
|
180
|
+
const userInfo = JSON.parse(userResp.body);
|
|
181
|
+
const personId = userInfo.sub || "";
|
|
182
|
+
if (!personId) {
|
|
183
|
+
throw new Error("Could not retrieve person ID from userinfo.");
|
|
184
|
+
}
|
|
185
|
+
console.log(`Person ID: ${personId}`);
|
|
186
|
+
// 4. Save credentials
|
|
187
|
+
const expiresAt = Math.floor(Date.now() / 1000) + (tokens.expires_in || 5184000);
|
|
188
|
+
const credentials = {
|
|
189
|
+
clientId,
|
|
190
|
+
clientSecret,
|
|
191
|
+
accessToken: tokens.access_token,
|
|
192
|
+
refreshToken: tokens.refresh_token || "",
|
|
193
|
+
personId,
|
|
194
|
+
expiresAt,
|
|
195
|
+
};
|
|
196
|
+
saveCredentials(credentials);
|
|
197
|
+
return credentials;
|
|
198
|
+
}
|
|
199
|
+
async function refreshAccessToken(creds) {
|
|
200
|
+
if (!creds.refreshToken) {
|
|
201
|
+
throw new Error("No refresh token available. Run 'linkedin-agent auth' again.");
|
|
202
|
+
}
|
|
203
|
+
console.log("Refreshing access token...");
|
|
204
|
+
const resp = await httpsPost("https://www.linkedin.com/oauth/v2/accessToken", new URLSearchParams({
|
|
205
|
+
grant_type: "refresh_token",
|
|
206
|
+
refresh_token: creds.refreshToken,
|
|
207
|
+
client_id: creds.clientId,
|
|
208
|
+
client_secret: creds.clientSecret,
|
|
209
|
+
}).toString(), { "Content-Type": "application/x-www-form-urlencoded" });
|
|
210
|
+
const tokens = JSON.parse(resp.body);
|
|
211
|
+
if (!tokens.access_token) {
|
|
212
|
+
throw new Error(`Token refresh failed: ${resp.body}`);
|
|
213
|
+
}
|
|
214
|
+
const updated = {
|
|
215
|
+
...creds,
|
|
216
|
+
accessToken: tokens.access_token,
|
|
217
|
+
refreshToken: tokens.refresh_token || creds.refreshToken,
|
|
218
|
+
expiresAt: Math.floor(Date.now() / 1000) + (tokens.expires_in || 5184000),
|
|
219
|
+
};
|
|
220
|
+
saveCredentials(updated);
|
|
221
|
+
console.log("Token refreshed successfully.");
|
|
222
|
+
return updated;
|
|
223
|
+
}
|
|
224
|
+
async function getValidCredentials() {
|
|
225
|
+
const creds = loadCredentials();
|
|
226
|
+
if (!creds) {
|
|
227
|
+
throw new Error("No credentials found. Run 'linkedin-agent auth' first.");
|
|
228
|
+
}
|
|
229
|
+
// Refresh if expiring within 1 day
|
|
230
|
+
const oneDayFromNow = Math.floor(Date.now() / 1000) + 86400;
|
|
231
|
+
if (creds.expiresAt < oneDayFromNow) {
|
|
232
|
+
return refreshAccessToken(creds);
|
|
233
|
+
}
|
|
234
|
+
return creds;
|
|
235
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getUserDataDir = getUserDataDir;
|
|
37
|
+
exports.launchBrowser = launchBrowser;
|
|
38
|
+
exports.ensureLoggedIn = ensureLoggedIn;
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const playwright_1 = require("playwright");
|
|
43
|
+
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
44
|
+
function getUserDataDir() {
|
|
45
|
+
const dir = path.join(os.homedir(), ".linkedin-agent", "chrome-data");
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
async function launchBrowser(userDataDir) {
|
|
50
|
+
return playwright_1.chromium.launchPersistentContext(userDataDir || getUserDataDir(), {
|
|
51
|
+
headless: false,
|
|
52
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
53
|
+
viewport: { width: 1280, height: 900 },
|
|
54
|
+
userAgent: USER_AGENT,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function ensureLoggedIn(page) {
|
|
58
|
+
await page.goto("https://www.linkedin.com/feed/", { waitUntil: "domcontentloaded" });
|
|
59
|
+
await page.waitForLoadState("networkidle").catch(() => { });
|
|
60
|
+
await page.waitForTimeout(1500);
|
|
61
|
+
const url = page.url();
|
|
62
|
+
if (url.includes("/login") || url.includes("/authwall") || url.includes("/checkpoint")) {
|
|
63
|
+
console.log("🔐 Login required. Please log in via the browser...");
|
|
64
|
+
await page.waitForFunction(() => {
|
|
65
|
+
const u = window.location.href;
|
|
66
|
+
return !u.includes("/login") && !u.includes("/authwall") && !u.includes("/checkpoint");
|
|
67
|
+
}, undefined, { timeout: 300_000 });
|
|
68
|
+
await page.waitForLoadState("networkidle").catch(() => { });
|
|
69
|
+
await page.waitForTimeout(1500);
|
|
70
|
+
}
|
|
71
|
+
console.log("✅ Session active.\n");
|
|
72
|
+
}
|