linkedin-automation-cli 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.
Files changed (314) hide show
  1. package/.env.example +12 -0
  2. package/.github/workflows/ci.yml +66 -0
  3. package/.github/workflows/publish.yml +48 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +10 -0
  7. package/AGENTS.md +294 -0
  8. package/CHANGELOG.md +40 -0
  9. package/GIT_RELEASE.md +167 -0
  10. package/LICENSE +21 -0
  11. package/Makefile +30 -0
  12. package/NPM_PUBLISHING.md +230 -0
  13. package/PYEOF +0 -0
  14. package/README.md +295 -0
  15. package/TESTING-GUIDE.md +151 -0
  16. package/cmd/linkedin/main.go +9 -0
  17. package/dist/agent/action-executor.d.ts +81 -0
  18. package/dist/agent/action-executor.d.ts.map +1 -0
  19. package/dist/agent/action-executor.js +170 -0
  20. package/dist/agent/action-executor.js.map +1 -0
  21. package/dist/agent/action-executor.test.d.ts +2 -0
  22. package/dist/agent/action-executor.test.d.ts.map +1 -0
  23. package/dist/agent/action-executor.test.js +366 -0
  24. package/dist/agent/action-executor.test.js.map +1 -0
  25. package/dist/agent/claude-client.d.ts +74 -0
  26. package/dist/agent/claude-client.d.ts.map +1 -0
  27. package/dist/agent/claude-client.js +314 -0
  28. package/dist/agent/claude-client.js.map +1 -0
  29. package/dist/agent/claude-client.test.d.ts +2 -0
  30. package/dist/agent/claude-client.test.d.ts.map +1 -0
  31. package/dist/agent/claude-client.test.js +590 -0
  32. package/dist/agent/claude-client.test.js.map +1 -0
  33. package/dist/agent/dom-extractor.d.ts +50 -0
  34. package/dist/agent/dom-extractor.d.ts.map +1 -0
  35. package/dist/agent/dom-extractor.js +374 -0
  36. package/dist/agent/dom-extractor.js.map +1 -0
  37. package/dist/agent/dom-extractor.test.d.ts +7 -0
  38. package/dist/agent/dom-extractor.test.d.ts.map +1 -0
  39. package/dist/agent/dom-extractor.test.js +504 -0
  40. package/dist/agent/dom-extractor.test.js.map +1 -0
  41. package/dist/agent/extension-client.d.ts +75 -0
  42. package/dist/agent/extension-client.d.ts.map +1 -0
  43. package/dist/agent/extension-client.js +245 -0
  44. package/dist/agent/extension-client.js.map +1 -0
  45. package/dist/agent/index.d.ts +8 -0
  46. package/dist/agent/index.d.ts.map +1 -0
  47. package/dist/agent/index.js +16 -0
  48. package/dist/agent/index.js.map +1 -0
  49. package/dist/agent/page-agent.d.ts +76 -0
  50. package/dist/agent/page-agent.d.ts.map +1 -0
  51. package/dist/agent/page-agent.js +236 -0
  52. package/dist/agent/page-agent.js.map +1 -0
  53. package/dist/agent/types.d.ts +236 -0
  54. package/dist/agent/types.d.ts.map +1 -0
  55. package/dist/agent/types.js +37 -0
  56. package/dist/agent/types.js.map +1 -0
  57. package/dist/cli/agent-commands.d.ts +3 -0
  58. package/dist/cli/agent-commands.d.ts.map +1 -0
  59. package/dist/cli/agent-commands.js +250 -0
  60. package/dist/cli/agent-commands.js.map +1 -0
  61. package/dist/cli/auth.d.ts +3 -0
  62. package/dist/cli/auth.d.ts.map +1 -0
  63. package/dist/cli/auth.js +288 -0
  64. package/dist/cli/auth.js.map +1 -0
  65. package/dist/cli/company.d.ts +3 -0
  66. package/dist/cli/company.d.ts.map +1 -0
  67. package/dist/cli/company.js +55 -0
  68. package/dist/cli/company.js.map +1 -0
  69. package/dist/cli/connection.d.ts +3 -0
  70. package/dist/cli/connection.d.ts.map +1 -0
  71. package/dist/cli/connection.js +79 -0
  72. package/dist/cli/connection.js.map +1 -0
  73. package/dist/cli/index.d.ts +7 -0
  74. package/dist/cli/index.d.ts.map +1 -0
  75. package/dist/cli/index.js +17 -0
  76. package/dist/cli/index.js.map +1 -0
  77. package/dist/cli/messages.d.ts +3 -0
  78. package/dist/cli/messages.d.ts.map +1 -0
  79. package/dist/cli/messages.js +268 -0
  80. package/dist/cli/messages.js.map +1 -0
  81. package/dist/cli/profile.d.ts +3 -0
  82. package/dist/cli/profile.d.ts.map +1 -0
  83. package/dist/cli/profile.js +81 -0
  84. package/dist/cli/profile.js.map +1 -0
  85. package/dist/cli/profile.test.d.ts +2 -0
  86. package/dist/cli/profile.test.d.ts.map +1 -0
  87. package/dist/cli/profile.test.js +15 -0
  88. package/dist/cli/profile.test.js.map +1 -0
  89. package/dist/cli/reply.d.ts +3 -0
  90. package/dist/cli/reply.d.ts.map +1 -0
  91. package/dist/cli/reply.js +129 -0
  92. package/dist/cli/reply.js.map +1 -0
  93. package/dist/core/audit.d.ts +17 -0
  94. package/dist/core/audit.d.ts.map +1 -0
  95. package/dist/core/audit.js +121 -0
  96. package/dist/core/audit.js.map +1 -0
  97. package/dist/core/audit.test.d.ts +2 -0
  98. package/dist/core/audit.test.d.ts.map +1 -0
  99. package/dist/core/audit.test.js +142 -0
  100. package/dist/core/audit.test.js.map +1 -0
  101. package/dist/core/browser-cookies.d.ts +19 -0
  102. package/dist/core/browser-cookies.d.ts.map +1 -0
  103. package/dist/core/browser-cookies.js +181 -0
  104. package/dist/core/browser-cookies.js.map +1 -0
  105. package/dist/core/browser.d.ts +50 -0
  106. package/dist/core/browser.d.ts.map +1 -0
  107. package/dist/core/browser.js +318 -0
  108. package/dist/core/browser.js.map +1 -0
  109. package/dist/core/config.d.ts +20 -0
  110. package/dist/core/config.d.ts.map +1 -0
  111. package/dist/core/config.js +103 -0
  112. package/dist/core/config.js.map +1 -0
  113. package/dist/core/config.test.d.ts +2 -0
  114. package/dist/core/config.test.d.ts.map +1 -0
  115. package/dist/core/config.test.js +111 -0
  116. package/dist/core/config.test.js.map +1 -0
  117. package/dist/core/storage.d.ts +19 -0
  118. package/dist/core/storage.d.ts.map +1 -0
  119. package/dist/core/storage.js +124 -0
  120. package/dist/core/storage.js.map +1 -0
  121. package/dist/core/storage.test.d.ts +2 -0
  122. package/dist/core/storage.test.d.ts.map +1 -0
  123. package/dist/core/storage.test.js +142 -0
  124. package/dist/core/storage.test.js.map +1 -0
  125. package/dist/index.d.ts +3 -0
  126. package/dist/index.d.ts.map +1 -0
  127. package/dist/index.js +63 -0
  128. package/dist/index.js.map +1 -0
  129. package/dist/linkedin/auth.d.ts +22 -0
  130. package/dist/linkedin/auth.d.ts.map +1 -0
  131. package/dist/linkedin/auth.js +167 -0
  132. package/dist/linkedin/auth.js.map +1 -0
  133. package/dist/linkedin/company-extractor.d.ts +36 -0
  134. package/dist/linkedin/company-extractor.d.ts.map +1 -0
  135. package/dist/linkedin/company-extractor.js +211 -0
  136. package/dist/linkedin/company-extractor.js.map +1 -0
  137. package/dist/linkedin/company-extractor.test.d.ts +2 -0
  138. package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
  139. package/dist/linkedin/company-extractor.test.js +52 -0
  140. package/dist/linkedin/company-extractor.test.js.map +1 -0
  141. package/dist/linkedin/connector.d.ts +45 -0
  142. package/dist/linkedin/connector.d.ts.map +1 -0
  143. package/dist/linkedin/connector.js +245 -0
  144. package/dist/linkedin/connector.js.map +1 -0
  145. package/dist/linkedin/message-sender.d.ts +32 -0
  146. package/dist/linkedin/message-sender.d.ts.map +1 -0
  147. package/dist/linkedin/message-sender.js +112 -0
  148. package/dist/linkedin/message-sender.js.map +1 -0
  149. package/dist/linkedin/messages.d.ts +78 -0
  150. package/dist/linkedin/messages.d.ts.map +1 -0
  151. package/dist/linkedin/messages.js +745 -0
  152. package/dist/linkedin/messages.js.map +1 -0
  153. package/dist/linkedin/profile.d.ts +37 -0
  154. package/dist/linkedin/profile.d.ts.map +1 -0
  155. package/dist/linkedin/profile.js +268 -0
  156. package/dist/linkedin/profile.js.map +1 -0
  157. package/dist/linkedin/profile.test.d.ts +2 -0
  158. package/dist/linkedin/profile.test.d.ts.map +1 -0
  159. package/dist/linkedin/profile.test.js +68 -0
  160. package/dist/linkedin/profile.test.js.map +1 -0
  161. package/dist/linkedin/reply.d.ts +21 -0
  162. package/dist/linkedin/reply.d.ts.map +1 -0
  163. package/dist/linkedin/reply.js +76 -0
  164. package/dist/linkedin/reply.js.map +1 -0
  165. package/dist/linkedin/selector-engine.d.ts +69 -0
  166. package/dist/linkedin/selector-engine.d.ts.map +1 -0
  167. package/dist/linkedin/selector-engine.js +339 -0
  168. package/dist/linkedin/selector-engine.js.map +1 -0
  169. package/dist/linkedin/selector-engine.test.d.ts +2 -0
  170. package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
  171. package/dist/linkedin/selector-engine.test.js +135 -0
  172. package/dist/linkedin/selector-engine.test.js.map +1 -0
  173. package/dist/linkedin/selectors.d.ts +65 -0
  174. package/dist/linkedin/selectors.d.ts.map +1 -0
  175. package/dist/linkedin/selectors.js +261 -0
  176. package/dist/linkedin/selectors.js.map +1 -0
  177. package/dist/templates/engine.d.ts +37 -0
  178. package/dist/templates/engine.d.ts.map +1 -0
  179. package/dist/templates/engine.js +215 -0
  180. package/dist/templates/engine.js.map +1 -0
  181. package/dist/templates/engine.test.d.ts +2 -0
  182. package/dist/templates/engine.test.d.ts.map +1 -0
  183. package/dist/templates/engine.test.js +212 -0
  184. package/dist/templates/engine.test.js.map +1 -0
  185. package/dist/templates/index.d.ts +2 -0
  186. package/dist/templates/index.d.ts.map +1 -0
  187. package/dist/templates/index.js +7 -0
  188. package/dist/templates/index.js.map +1 -0
  189. package/dist/types/index.d.ts +113 -0
  190. package/dist/types/index.d.ts.map +1 -0
  191. package/dist/types/index.js +3 -0
  192. package/dist/types/index.js.map +1 -0
  193. package/dist/types/index.test.d.ts +2 -0
  194. package/dist/types/index.test.d.ts.map +1 -0
  195. package/dist/types/index.test.js +90 -0
  196. package/dist/types/index.test.js.map +1 -0
  197. package/dist/utils/paths.d.ts +8 -0
  198. package/dist/utils/paths.d.ts.map +1 -0
  199. package/dist/utils/paths.js +68 -0
  200. package/dist/utils/paths.js.map +1 -0
  201. package/dist/utils/rate-limiter.d.ts +22 -0
  202. package/dist/utils/rate-limiter.d.ts.map +1 -0
  203. package/dist/utils/rate-limiter.js +57 -0
  204. package/dist/utils/rate-limiter.js.map +1 -0
  205. package/dist/utils/retry.d.ts +18 -0
  206. package/dist/utils/retry.d.ts.map +1 -0
  207. package/dist/utils/retry.js +49 -0
  208. package/dist/utils/retry.js.map +1 -0
  209. package/docs/connection-command.md +52 -0
  210. package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
  211. package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
  212. package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
  213. package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
  214. package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
  215. package/docs/plans/2026-02-28-messages-show-design.md +243 -0
  216. package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
  217. package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
  218. package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
  219. package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
  220. package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
  221. package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
  222. package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
  223. package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
  224. package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
  225. package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
  226. package/eslint.config.mjs +58 -0
  227. package/go.mod +9 -0
  228. package/go.sum +10 -0
  229. package/import-cookies.js +376 -0
  230. package/internal/cmd/actions.go +123 -0
  231. package/internal/cmd/auth.go +108 -0
  232. package/internal/cmd/connect.go +42 -0
  233. package/internal/cmd/message.go +44 -0
  234. package/internal/cmd/people.go +454 -0
  235. package/internal/cmd/profiles.go +121 -0
  236. package/internal/cmd/root.go +89 -0
  237. package/internal/cmd/sequence.go +192 -0
  238. package/internal/config/config.go +187 -0
  239. package/internal/config/config_test.go +121 -0
  240. package/internal/config/profile.go +65 -0
  241. package/internal/linkedin/navigator.go +195 -0
  242. package/internal/linkedin/selectors.go +39 -0
  243. package/internal/linkedin/validator.go +69 -0
  244. package/internal/pinchtab/client.go +183 -0
  245. package/internal/pinchtab/client_test.go +67 -0
  246. package/internal/pinchtab/types.go +50 -0
  247. package/internal/ratelimit/limiter.go +115 -0
  248. package/internal/ratelimit/limits.go +32 -0
  249. package/package.json +67 -0
  250. package/release.sh +66 -0
  251. package/scripts/debug-linkedin.js +156 -0
  252. package/scripts/debug-login.js +193 -0
  253. package/scripts/extract-from-edge.js +96 -0
  254. package/scripts/import-cookies.js +101 -0
  255. package/scripts/poc-show-data.js +205 -0
  256. package/scripts/proof-of-access.js +87 -0
  257. package/scripts/prove-connection.js +110 -0
  258. package/scripts/show-linkedin-data.js +173 -0
  259. package/src/agent/action-executor.test.ts +464 -0
  260. package/src/agent/action-executor.ts +203 -0
  261. package/src/agent/claude-client.test.ts +707 -0
  262. package/src/agent/claude-client.ts +422 -0
  263. package/src/agent/dom-extractor.test.ts +574 -0
  264. package/src/agent/dom-extractor.ts +437 -0
  265. package/src/agent/extension-client.ts +306 -0
  266. package/src/agent/index.ts +28 -0
  267. package/src/agent/page-agent.ts +292 -0
  268. package/src/agent/types.ts +288 -0
  269. package/src/cli/agent-commands.ts +274 -0
  270. package/src/cli/auth.ts +343 -0
  271. package/src/cli/company.ts +66 -0
  272. package/src/cli/connection.ts +89 -0
  273. package/src/cli/index.ts +7 -0
  274. package/src/cli/messages.ts +338 -0
  275. package/src/cli/profile.test.ts +14 -0
  276. package/src/cli/profile.ts +95 -0
  277. package/src/cli/reply.ts +110 -0
  278. package/src/core/audit.test.ts +134 -0
  279. package/src/core/audit.ts +98 -0
  280. package/src/core/browser-cookies.ts +203 -0
  281. package/src/core/browser.ts +304 -0
  282. package/src/core/config.test.ts +90 -0
  283. package/src/core/config.ts +81 -0
  284. package/src/core/storage.test.ts +129 -0
  285. package/src/core/storage.ts +100 -0
  286. package/src/index.ts +70 -0
  287. package/src/linkedin/auth.ts +218 -0
  288. package/src/linkedin/company-extractor.test.ts +58 -0
  289. package/src/linkedin/company-extractor.ts +222 -0
  290. package/src/linkedin/connector.ts +336 -0
  291. package/src/linkedin/message-sender.ts +141 -0
  292. package/src/linkedin/messages.ts +894 -0
  293. package/src/linkedin/profile.test.ts +79 -0
  294. package/src/linkedin/profile.ts +314 -0
  295. package/src/linkedin/reply.ts +96 -0
  296. package/src/linkedin/selector-engine.test.ts +167 -0
  297. package/src/linkedin/selector-engine.ts +393 -0
  298. package/src/linkedin/selectors.ts +268 -0
  299. package/src/templates/defaults/followup.txt +14 -0
  300. package/src/templates/defaults/meeting.txt +16 -0
  301. package/src/templates/defaults/welcome.txt +14 -0
  302. package/src/templates/engine.test.ts +228 -0
  303. package/src/templates/engine.ts +208 -0
  304. package/src/templates/index.ts +1 -0
  305. package/src/types/index.test.ts +94 -0
  306. package/src/types/index.ts +143 -0
  307. package/src/types/sql.js.d.ts +23 -0
  308. package/src/utils/paths.ts +33 -0
  309. package/src/utils/rate-limiter.ts +75 -0
  310. package/src/utils/retry.ts +78 -0
  311. package/test-cli.sh +85 -0
  312. package/test-real-data.sh +97 -0
  313. package/tsconfig.json +23 -0
  314. package/vitest.config.ts +35 -0
@@ -0,0 +1,173 @@
1
+ const { chromium } = require('playwright');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+
6
+ async function showLinkedInData() {
7
+ console.log('🎯 Fetching Your LinkedIn Data');
8
+ console.log('==============================\n');
9
+
10
+ // Decrypt session
11
+ const sessionFile = path.join(require('os').homedir(), '.linkedin-cli/sessions/linkedin-session.json');
12
+ const encrypted = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
13
+
14
+ const machineData = [process.env.USER, process.env.HOME, process.platform].join('|');
15
+ const salt = Buffer.from(encrypted.salt, 'base64');
16
+ const key = crypto.pbkdf2Sync(machineData, salt, 100000, 32, 'sha256');
17
+ const iv = Buffer.from(encrypted.iv, 'base64');
18
+ const encryptedData = Buffer.from(encrypted.encrypted, 'base64');
19
+ const tag = Buffer.from(encrypted.tag, 'base64');
20
+
21
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
22
+ decipher.setAuthTag(tag);
23
+ const session = JSON.parse(Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString());
24
+
25
+ console.log('✅ Session decrypted successfully');
26
+ console.log(`📅 Session created: ${new Date(session.timestamp).toLocaleString()}`);
27
+ console.log(`🍪 Cookies: ${session.cookies.length} LinkedIn cookies loaded\n`);
28
+
29
+ // Launch Edge browser
30
+ console.log('🌐 Opening browser...');
31
+ const browser = await chromium.launch({
32
+ headless: true,
33
+ executablePath: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
34
+ });
35
+
36
+ const context = await browser.newContext({
37
+ userAgent: session.userAgent
38
+ });
39
+
40
+ // Set cookies
41
+ await context.addCookies(session.cookies.map(c => ({
42
+ name: c.name,
43
+ value: c.value,
44
+ domain: c.domain,
45
+ path: c.path || '/',
46
+ secure: c.secure,
47
+ httpOnly: false,
48
+ sameSite: 'Lax'
49
+ })));
50
+
51
+ // Check Feed
52
+ console.log('\n📰 CHECKING LINKEDIN FEED...');
53
+ console.log('─────────────────────────────────────');
54
+ const feedPage = await context.newPage();
55
+ await feedPage.goto('https://www.linkedin.com/feed/', { waitUntil: 'networkidle', timeout: 30000 });
56
+
57
+ // Wait for feed to load
58
+ await feedPage.waitForTimeout(5000);
59
+
60
+ // Get feed posts (try multiple selectors)
61
+ const posts = await feedPage.evaluate(() => {
62
+ const selectors = [
63
+ '.feed-shared-update-v2',
64
+ '[data-testid="feed-shared-update-v2"]',
65
+ '.scaffold-finite-scroll__content > div',
66
+ '[class*="update-v2"]'
67
+ ];
68
+
69
+ for (const selector of selectors) {
70
+ const elements = document.querySelectorAll(selector);
71
+ if (elements.length > 0) {
72
+ return Array.from(elements).slice(0, 3).map(el => {
73
+ const authorEl = el.querySelector('[class*="actor"], [class*="author"], [class*="name"]');
74
+ const textEl = el.querySelector('[class*="update-text"], [class*="content"], p');
75
+ return {
76
+ author: authorEl?.innerText?.trim() || 'Unknown',
77
+ text: textEl?.innerText?.trim()?.substring(0, 150) || 'No text'
78
+ };
79
+ });
80
+ }
81
+ }
82
+ return [];
83
+ });
84
+
85
+ if (posts.length > 0) {
86
+ console.log(`✅ Found ${posts.length} posts in your feed:\n`);
87
+ posts.forEach((post, i) => {
88
+ console.log(`${i + 1}. 👤 ${post.author}`);
89
+ console.log(` 📝 ${post.text}...\n`);
90
+ });
91
+ } else {
92
+ console.log('⚠️ Could not extract feed posts');
93
+ }
94
+
95
+ // Check Messages
96
+ console.log('\n💬 CHECKING PRIVATE MESSAGES...');
97
+ console.log('─────────────────────────────────────');
98
+ const msgPage = await context.newPage();
99
+ await msgPage.goto('https://www.linkedin.com/messaging/', { waitUntil: 'networkidle', timeout: 30000 });
100
+
101
+ await msgPage.waitForTimeout(5000);
102
+
103
+ // Get conversations
104
+ const conversations = await msgPage.evaluate(() => {
105
+ const selectors = [
106
+ '.msg-conversation-card',
107
+ '[data-testid="conversation-card"]',
108
+ '[class*="conversation-card"]',
109
+ '[class*="msg-conversation-listitem"]'
110
+ ];
111
+
112
+ for (const selector of selectors) {
113
+ const elements = document.querySelectorAll(selector);
114
+ if (elements.length > 0) {
115
+ return Array.from(elements).slice(0, 5).map(el => {
116
+ const nameSelectors = ['[class*="participant"]', '[class*="name"]', 'h3', 'span'];
117
+ const previewSelectors = ['[class*="preview"]', '[class*="message"]', 'p'];
118
+
119
+ let name = 'Unknown';
120
+ let preview = '';
121
+
122
+ for (const ns of nameSelectors) {
123
+ const ne = el.querySelector(ns);
124
+ if (ne?.innerText?.trim()) {
125
+ name = ne.innerText.trim();
126
+ break;
127
+ }
128
+ }
129
+
130
+ for (const ps of previewSelectors) {
131
+ const pe = el.querySelector(ps);
132
+ if (pe?.innerText?.trim()) {
133
+ preview = pe.innerText.trim();
134
+ break;
135
+ }
136
+ }
137
+
138
+ const isUnread = el.querySelector('[class*="unread"]') !== null;
139
+
140
+ return { name, preview: preview?.substring(0, 100), isUnread };
141
+ });
142
+ }
143
+ }
144
+ return [];
145
+ });
146
+
147
+ if (conversations.length > 0) {
148
+ console.log(`✅ Found ${conversations.length} conversations:\n`);
149
+ conversations.forEach((conv, i) => {
150
+ const unreadBadge = conv.isUnread ? ' 🔴 UNREAD' : '';
151
+ console.log(`${i + 1}. 👤 ${conv.name}${unreadBadge}`);
152
+ if (conv.preview) {
153
+ console.log(` 💬 ${conv.preview}...\n`);
154
+ }
155
+ });
156
+ } else {
157
+ console.log('⚠️ No conversations found');
158
+ console.log(' (LinkedIn might have changed their page structure)');
159
+ }
160
+
161
+ await browser.close();
162
+
163
+ console.log('\n✅ DONE!');
164
+ console.log('═════════════════════════════════════');
165
+ console.log('Your LinkedIn CLI is WORKING! 🎉');
166
+ console.log('═════════════════════════════════════');
167
+ }
168
+
169
+ showLinkedInData().catch(error => {
170
+ console.error('❌ Error:', error.message);
171
+ console.error(error.stack);
172
+ process.exit(1);
173
+ });
@@ -0,0 +1,464 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ActionExecutor, createActionExecutor } from './action-executor';
3
+ import type { Page } from 'playwright';
4
+ import type {
5
+ DOMRepresentation,
6
+ Action,
7
+ ClickAction,
8
+ TypeAction,
9
+ WaitAction,
10
+ NavigateAction,
11
+ } from './types';
12
+
13
+ describe('ActionExecutor', () => {
14
+ let mockPage: Page;
15
+ let mockDOM: DOMRepresentation;
16
+ let executor: ActionExecutor;
17
+
18
+ beforeEach(() => {
19
+ // Create mock Page
20
+ mockPage = {
21
+ url: vi.fn().mockReturnValue('https://linkedin.com/in/test-user'),
22
+ title: vi.fn().mockResolvedValue('Test User - LinkedIn'),
23
+ mouse: {
24
+ click: vi.fn().mockResolvedValue(undefined),
25
+ },
26
+ keyboard: {
27
+ type: vi.fn().mockResolvedValue(undefined),
28
+ },
29
+ waitForTimeout: vi.fn().mockResolvedValue(undefined),
30
+ goto: vi.fn().mockResolvedValue(undefined),
31
+ } as unknown as Page;
32
+
33
+ // Create mock DOM representation
34
+ mockDOM = {
35
+ url: 'https://linkedin.com/in/test-user',
36
+ title: 'Test User - LinkedIn',
37
+ elements: [
38
+ {
39
+ id: 'btn-connect',
40
+ tag: 'button',
41
+ role: 'button',
42
+ ariaLabel: 'Connect',
43
+ text: 'Connect',
44
+ visible: true,
45
+ enabled: true,
46
+ bbox: { x: 100, y: 200, width: 120, height: 40 },
47
+ },
48
+ {
49
+ id: 'input-message',
50
+ tag: 'textarea',
51
+ role: 'textbox',
52
+ text: '',
53
+ visible: true,
54
+ enabled: true,
55
+ bbox: { x: 50, y: 300, width: 400, height: 100 },
56
+ },
57
+ {
58
+ id: 'btn-submit',
59
+ tag: 'button',
60
+ text: 'Submit',
61
+ visible: true,
62
+ enabled: true,
63
+ bbox: { x: 200, y: 420, width: 80, height: 35 },
64
+ },
65
+ ],
66
+ metadata: {
67
+ pageType: 'profile',
68
+ profileName: 'Test User',
69
+ connectionState: 'not_connected',
70
+ },
71
+ };
72
+
73
+ executor = new ActionExecutor(mockPage, mockDOM);
74
+ });
75
+
76
+ describe('constructor', () => {
77
+ it('should create an ActionExecutor with page and DOM', () => {
78
+ const executor = new ActionExecutor(mockPage, mockDOM);
79
+ expect(executor).toBeInstanceOf(ActionExecutor);
80
+ });
81
+
82
+ it('should create via factory function', () => {
83
+ const executor = createActionExecutor(mockPage, mockDOM);
84
+ expect(executor).toBeInstanceOf(ActionExecutor);
85
+ });
86
+ });
87
+
88
+ describe('execute - click action', () => {
89
+ it('should execute click action with correct coordinates', async () => {
90
+ const action: ClickAction = {
91
+ type: 'click',
92
+ elementId: 'btn-connect',
93
+ description: 'Click connect button',
94
+ };
95
+
96
+ const result = await executor.execute(action);
97
+
98
+ expect(result.success).toBe(true);
99
+ expect(result.error).toBeUndefined();
100
+
101
+ // Verify click was called with center coordinates
102
+ // centerX = 100 + 120/2 = 160, centerY = 200 + 40/2 = 220
103
+ expect(mockPage.mouse.click).toHaveBeenCalledWith(160, 220);
104
+ });
105
+
106
+ it('should handle click on element with different bbox', async () => {
107
+ const action: ClickAction = {
108
+ type: 'click',
109
+ elementId: 'btn-submit',
110
+ description: 'Click submit button',
111
+ };
112
+
113
+ const result = await executor.execute(action);
114
+
115
+ expect(result.success).toBe(true);
116
+ // centerX = 200 + 80/2 = 240, centerY = 420 + 35/2 = 437.5
117
+ expect(mockPage.mouse.click).toHaveBeenCalledWith(240, 437.5);
118
+ });
119
+
120
+ it('should return error when element not found', async () => {
121
+ const action: ClickAction = {
122
+ type: 'click',
123
+ elementId: 'non-existent-id',
124
+ description: 'Click non-existent button',
125
+ };
126
+
127
+ const result = await executor.execute(action);
128
+
129
+ expect(result.success).toBe(false);
130
+ expect(result.error).toContain('Element not found');
131
+ expect(result.error).toContain('non-existent-id');
132
+ expect(mockPage.mouse.click).not.toHaveBeenCalled();
133
+ });
134
+ });
135
+
136
+ describe('execute - type action', () => {
137
+ it('should execute type action by clicking then typing', async () => {
138
+ const action: TypeAction = {
139
+ type: 'type',
140
+ elementId: 'input-message',
141
+ text: 'Hello, this is a test message',
142
+ description: 'Type message in textarea',
143
+ };
144
+
145
+ const result = await executor.execute(action);
146
+
147
+ expect(result.success).toBe(true);
148
+
149
+ // Should click to focus first
150
+ // centerX = 50 + 400/2 = 250, centerY = 300 + 100/2 = 350
151
+ expect(mockPage.mouse.click).toHaveBeenCalledWith(250, 350);
152
+
153
+ // Should wait 100ms for focus
154
+ expect(mockPage.waitForTimeout).toHaveBeenCalledWith(100);
155
+
156
+ // Should type the text
157
+ expect(mockPage.keyboard.type).toHaveBeenCalledWith('Hello, this is a test message');
158
+ });
159
+
160
+ it('should handle empty text', async () => {
161
+ const action: TypeAction = {
162
+ type: 'type',
163
+ elementId: 'input-message',
164
+ text: '',
165
+ description: 'Type empty string',
166
+ };
167
+
168
+ const result = await executor.execute(action);
169
+
170
+ expect(result.success).toBe(true);
171
+ expect(mockPage.keyboard.type).toHaveBeenCalledWith('');
172
+ });
173
+
174
+ it('should return error when element not found for type action', async () => {
175
+ const action: TypeAction = {
176
+ type: 'type',
177
+ elementId: 'non-existent-input',
178
+ text: 'Some text',
179
+ description: 'Type in non-existent field',
180
+ };
181
+
182
+ const result = await executor.execute(action);
183
+
184
+ expect(result.success).toBe(false);
185
+ expect(result.error).toContain('Element not found');
186
+ expect(mockPage.mouse.click).not.toHaveBeenCalled();
187
+ expect(mockPage.keyboard.type).not.toHaveBeenCalled();
188
+ });
189
+ });
190
+
191
+ describe('execute - wait action', () => {
192
+ it('should execute wait action with specified duration', async () => {
193
+ const action: WaitAction = {
194
+ type: 'wait',
195
+ durationMs: 2000,
196
+ description: 'Wait for modal to appear',
197
+ };
198
+
199
+ const result = await executor.execute(action);
200
+
201
+ expect(result.success).toBe(true);
202
+ expect(mockPage.waitForTimeout).toHaveBeenCalledWith(2000);
203
+ });
204
+
205
+ it('should handle zero duration wait', async () => {
206
+ const action: WaitAction = {
207
+ type: 'wait',
208
+ durationMs: 0,
209
+ description: 'Wait 0ms',
210
+ };
211
+
212
+ const result = await executor.execute(action);
213
+
214
+ expect(result.success).toBe(true);
215
+ expect(mockPage.waitForTimeout).toHaveBeenCalledWith(0);
216
+ });
217
+
218
+ it('should handle short duration wait', async () => {
219
+ const action: WaitAction = {
220
+ type: 'wait',
221
+ durationMs: 100,
222
+ description: 'Wait 100ms',
223
+ };
224
+
225
+ const result = await executor.execute(action);
226
+
227
+ expect(result.success).toBe(true);
228
+ expect(mockPage.waitForTimeout).toHaveBeenCalledWith(100);
229
+ });
230
+ });
231
+
232
+ describe('execute - navigate action', () => {
233
+ it('should execute navigate action with correct options', async () => {
234
+ const action: NavigateAction = {
235
+ type: 'navigate',
236
+ url: 'https://linkedin.com/in/another-user',
237
+ description: 'Navigate to another profile',
238
+ };
239
+
240
+ const result = await executor.execute(action);
241
+
242
+ expect(result.success).toBe(true);
243
+ expect(mockPage.goto).toHaveBeenCalledWith('https://linkedin.com/in/another-user', {
244
+ waitUntil: 'domcontentloaded',
245
+ timeout: 30000,
246
+ });
247
+ });
248
+
249
+ it('should handle navigation to root URL', async () => {
250
+ const action: NavigateAction = {
251
+ type: 'navigate',
252
+ url: 'https://linkedin.com',
253
+ description: 'Navigate to LinkedIn home',
254
+ };
255
+
256
+ const result = await executor.execute(action);
257
+
258
+ expect(result.success).toBe(true);
259
+ expect(mockPage.goto).toHaveBeenCalledWith('https://linkedin.com', {
260
+ waitUntil: 'domcontentloaded',
261
+ timeout: 30000,
262
+ });
263
+ });
264
+ });
265
+
266
+ describe('execute - unknown action type', () => {
267
+ it('should return error for unknown action type', async () => {
268
+ const action = {
269
+ type: 'unknown' as const,
270
+ description: 'Unknown action',
271
+ } as unknown as Action;
272
+
273
+ const result = await executor.execute(action);
274
+
275
+ expect(result.success).toBe(false);
276
+ expect(result.error).toContain('Unknown action type');
277
+ });
278
+ });
279
+
280
+ describe('execute - error handling', () => {
281
+ it('should handle mouse click errors', async () => {
282
+ const errorMessage = 'Element not interactable';
283
+ vi.mocked(mockPage.mouse.click).mockRejectedValueOnce(new Error(errorMessage));
284
+
285
+ const action: ClickAction = {
286
+ type: 'click',
287
+ elementId: 'btn-connect',
288
+ description: 'Click button',
289
+ };
290
+
291
+ const result = await executor.execute(action);
292
+
293
+ expect(result.success).toBe(false);
294
+ expect(result.error).toBe(errorMessage);
295
+ });
296
+
297
+ it('should handle keyboard type errors', async () => {
298
+ const errorMessage = 'Element is not focused';
299
+ vi.mocked(mockPage.keyboard.type).mockRejectedValueOnce(new Error(errorMessage));
300
+
301
+ const action: TypeAction = {
302
+ type: 'type',
303
+ elementId: 'input-message',
304
+ text: 'Test',
305
+ description: 'Type text',
306
+ };
307
+
308
+ const result = await executor.execute(action);
309
+
310
+ expect(result.success).toBe(false);
311
+ expect(result.error).toBe(errorMessage);
312
+ });
313
+
314
+ it('should handle navigation errors', async () => {
315
+ const errorMessage = 'net::ERR_NAME_NOT_RESOLVED';
316
+ vi.mocked(mockPage.goto).mockRejectedValueOnce(new Error(errorMessage));
317
+
318
+ const action: NavigateAction = {
319
+ type: 'navigate',
320
+ url: 'https://invalid-url',
321
+ description: 'Navigate to invalid URL',
322
+ };
323
+
324
+ const result = await executor.execute(action);
325
+
326
+ expect(result.success).toBe(false);
327
+ expect(result.error).toBe(errorMessage);
328
+ });
329
+
330
+ it('should handle non-Error exceptions', async () => {
331
+ vi.mocked(mockPage.mouse.click).mockRejectedValueOnce('String error');
332
+
333
+ const action: ClickAction = {
334
+ type: 'click',
335
+ elementId: 'btn-connect',
336
+ description: 'Click button',
337
+ };
338
+
339
+ const result = await executor.execute(action);
340
+
341
+ expect(result.success).toBe(false);
342
+ expect(result.error).toBe('String error');
343
+ });
344
+ });
345
+
346
+ describe('executeAll', () => {
347
+ it('should execute multiple actions sequentially', async () => {
348
+ const actions: Action[] = [
349
+ { type: 'click', elementId: 'btn-connect', description: 'Click connect' },
350
+ { type: 'wait', durationMs: 1000, description: 'Wait for modal' },
351
+ { type: 'click', elementId: 'btn-submit', description: 'Click submit' },
352
+ ];
353
+
354
+ const results = await executor.executeAll(actions);
355
+
356
+ expect(results).toHaveLength(3);
357
+ expect(results[0].success).toBe(true);
358
+ expect(results[0].action.type).toBe('click');
359
+ expect(results[1].success).toBe(true);
360
+ expect(results[1].action.type).toBe('wait');
361
+ expect(results[2].success).toBe(true);
362
+ expect(results[2].action.type).toBe('click');
363
+
364
+ // Each result should have a timestamp
365
+ results.forEach((result) => {
366
+ expect(result.timestamp).toBeInstanceOf(Date);
367
+ });
368
+ });
369
+
370
+ it('should add 500ms delay between actions', async () => {
371
+ const actions: Action[] = [
372
+ { type: 'click', elementId: 'btn-connect', description: 'Click connect' },
373
+ { type: 'click', elementId: 'btn-submit', description: 'Click submit' },
374
+ ];
375
+
376
+ await executor.executeAll(actions);
377
+
378
+ // Should have waitForTimeout called for the 500ms delay between actions
379
+ // plus potentially 100ms for type actions (not in this test)
380
+ const waitCalls = vi.mocked(mockPage.waitForTimeout).mock.calls;
381
+ const hasDelay = waitCalls.some((call) => call[0] === 500);
382
+ expect(hasDelay).toBe(true);
383
+ });
384
+
385
+ it('should stop on first failure', async () => {
386
+ const actions: Action[] = [
387
+ { type: 'click', elementId: 'btn-connect', description: 'Click connect' },
388
+ { type: 'click', elementId: 'non-existent', description: 'Click non-existent' },
389
+ { type: 'click', elementId: 'btn-submit', description: 'Should not execute' },
390
+ ];
391
+
392
+ const results = await executor.executeAll(actions);
393
+
394
+ // Should only have 2 results (stopped after failure)
395
+ expect(results).toHaveLength(2);
396
+ expect(results[0].success).toBe(true);
397
+ expect(results[1].success).toBe(false);
398
+ expect(results[1].error).toContain('Element not found');
399
+
400
+ // Third action should not have been executed
401
+ expect(mockPage.mouse.click).toHaveBeenCalledTimes(1);
402
+ });
403
+
404
+ it('should log each action before execution', async () => {
405
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
406
+
407
+ const actions: Action[] = [
408
+ { type: 'click', elementId: 'btn-connect', description: 'Click connect button' },
409
+ ];
410
+
411
+ await executor.executeAll(actions);
412
+
413
+ expect(consoleSpy).toHaveBeenCalledWith('Executing action: Click connect button');
414
+
415
+ consoleSpy.mockRestore();
416
+ });
417
+
418
+ it('should log error when action fails', async () => {
419
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
420
+
421
+ const actions: Action[] = [
422
+ { type: 'click', elementId: 'non-existent', description: 'Click non-existent' },
423
+ ];
424
+
425
+ await executor.executeAll(actions);
426
+
427
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Action failed:'));
428
+
429
+ consoleErrorSpy.mockRestore();
430
+ });
431
+
432
+ it('should handle empty action array', async () => {
433
+ const results = await executor.executeAll([]);
434
+
435
+ expect(results).toHaveLength(0);
436
+ });
437
+
438
+ it('should handle single action', async () => {
439
+ const actions: Action[] = [{ type: 'wait', durationMs: 500, description: 'Wait briefly' }];
440
+
441
+ const results = await executor.executeAll(actions);
442
+
443
+ expect(results).toHaveLength(1);
444
+ expect(results[0].success).toBe(true);
445
+
446
+ // No delay after the last action
447
+ const waitCalls = vi.mocked(mockPage.waitForTimeout).mock.calls;
448
+ expect(waitCalls).toHaveLength(1); // Only the wait action itself, no delay after
449
+ });
450
+
451
+ it('should include error in ExecutedAction when action fails', async () => {
452
+ const actions: Action[] = [
453
+ { type: 'click', elementId: 'non-existent', description: 'Click non-existent' },
454
+ ];
455
+
456
+ const results = await executor.executeAll(actions);
457
+
458
+ expect(results[0].success).toBe(false);
459
+ expect(results[0].error).toBeDefined();
460
+ expect(results[0].error).toContain('Element not found');
461
+ expect(results[0].timestamp).toBeInstanceOf(Date);
462
+ });
463
+ });
464
+ });