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 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
+ }
@@ -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
+ }