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,745 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LinkedInMessages = void 0;
4
+ const selector_engine_1 = require("./selector-engine");
5
+ const selectors_1 = require("./selectors");
6
+ class LinkedInMessages {
7
+ page;
8
+ selectorEngine;
9
+ constructor(page, _browser) {
10
+ this.page = page;
11
+ this.selectorEngine = new selector_engine_1.SelectorEngine(page);
12
+ }
13
+ /**
14
+ * Navigate to LinkedIn messaging page
15
+ */
16
+ async navigateToMessages() {
17
+ await this.page.goto('https://www.linkedin.com/messaging/', {
18
+ waitUntil: 'domcontentloaded',
19
+ timeout: 10000,
20
+ });
21
+ await this.page.waitForTimeout(1000);
22
+ }
23
+ /**
24
+ * Get list of conversation threads
25
+ */
26
+ async getThreads(options = {}) {
27
+ // Navigation is now handled by the caller - we're already on the messaging page
28
+ const limit = options.limit ?? 20;
29
+ const threads = [];
30
+ // Wait for conversation list to load
31
+ const listResult = await this.selectorEngine.findElement('messages', 'conversationList', {
32
+ timeout: 10000,
33
+ });
34
+ if (!listResult.element) {
35
+ console.warn('Could not find conversation list');
36
+ return threads;
37
+ }
38
+ // Get all conversation items using Playwright locators (more reliable than CSS selectors)
39
+ // LinkedIn uses dynamic class names, so we use multiple strategies
40
+ // Strategy 1: Look for li elements with msg-conversation-listitem class (the actual list items)
41
+ const listItems = await this.page.$$('[class*="msg-conversation-listitem"]');
42
+ // Get the conversation card div inside each list item
43
+ let conversationItems = [];
44
+ for (const listItem of listItems) {
45
+ const card = await listItem.$('.msg-conversation-card');
46
+ if (card) {
47
+ conversationItems.push(card);
48
+ }
49
+ }
50
+ // Strategy 2: Try data-testid
51
+ if (conversationItems.length === 0) {
52
+ conversationItems = await this.page.$$('[data-testid="conversation-card"]');
53
+ }
54
+ // Strategy 3: Try by ID pattern (conversation-card-ember*)
55
+ if (conversationItems.length === 0) {
56
+ conversationItems = await this.page.$$('[id^="conversation-card-"]');
57
+ }
58
+ // Strategy 4: Try role-based listitem query
59
+ if (conversationItems.length === 0) {
60
+ const allListItems = await this.page.$$('[role="listitem"]');
61
+ for (const item of allListItems) {
62
+ const text = await item.evaluate((el) => el.textContent || '');
63
+ // Conversation cards typically have participant names and message previews
64
+ if (text.length > 5 && (text.includes(':') || text.match(/\d+[mhd]/))) {
65
+ conversationItems.push(item);
66
+ }
67
+ }
68
+ }
69
+ // Final fallback: get all children of the conversation list that look like cards
70
+ if (conversationItems.length === 0 && listResult.element) {
71
+ const allChildren = await listResult.element.$$('* > *');
72
+ for (const child of allChildren) {
73
+ const className = await child.evaluate((el) => {
74
+ // Handle both SVG and HTML elements
75
+ if ('className' in el && typeof el.className === 'string') {
76
+ return el.className;
77
+ }
78
+ // For SVG elements, className might be an object
79
+ if ('className' in el && el.className && 'baseVal' in el.className) {
80
+ return el.className.baseVal;
81
+ }
82
+ return '';
83
+ });
84
+ // LinkedIn conversation cards typically have specific patterns
85
+ if (className &&
86
+ (className.includes('msg-') ||
87
+ className.includes('conversation') ||
88
+ className.includes('artdeco'))) {
89
+ conversationItems.push(child);
90
+ }
91
+ }
92
+ }
93
+ for (let i = 0; i < Math.min(conversationItems.length, limit); i++) {
94
+ const item = conversationItems[i];
95
+ const thread = await this.parseThreadItem(item);
96
+ if (thread) {
97
+ // Filter by unread if requested
98
+ if (options.unreadOnly && thread.unreadCount === 0) {
99
+ continue;
100
+ }
101
+ threads.push(thread);
102
+ }
103
+ }
104
+ return threads;
105
+ }
106
+ /**
107
+ * Parse a single conversation item element into a Thread
108
+ */
109
+ async parseThreadItem(item) {
110
+ try {
111
+ // Extract thread ID from data attribute or URL
112
+ const threadId = await item.evaluate((el) => {
113
+ const element = el;
114
+ // Strategy 1: Try to get from data-conversation-urn attribute
115
+ const dataId = element.getAttribute('data-conversation-urn');
116
+ if (dataId)
117
+ return dataId;
118
+ // Strategy 2: Try data-urn variant
119
+ const dataUrn = element.getAttribute('data-urn');
120
+ if (dataUrn) {
121
+ const match = dataUrn.match(/thread:(\d+)/);
122
+ if (match)
123
+ return match[1];
124
+ }
125
+ // Strategy 3: Look for data-event-urn in descendants (contains thread ID)
126
+ // Format: urn:li:msg_message:(urn:li:fsd_profile:...,2-<threadId>)
127
+ const eventUrnEl = element.querySelector('[data-event-urn]');
128
+ if (eventUrnEl) {
129
+ const eventUrn = eventUrnEl.getAttribute('data-event-urn');
130
+ if (eventUrn) {
131
+ // Extract thread ID from format: urn:li:msg_message:(...,2-<base64id>)
132
+ const match = eventUrn.match(/,2-([A-Za-z0-9+/=]+)/);
133
+ if (match) {
134
+ return `2-${match[1]}`;
135
+ }
136
+ }
137
+ }
138
+ // Strategy 4: Try to extract from link inside the element
139
+ const link = element.querySelector('a[href*="/messaging/thread/"]');
140
+ if (link) {
141
+ const href = link.getAttribute('href');
142
+ if (href) {
143
+ const match = href.match(/\/messaging\/thread\/([^/]+)/);
144
+ if (match)
145
+ return match[1];
146
+ }
147
+ }
148
+ // Strategy 5: Try to get from parent link
149
+ const parentLink = element.closest('a[href*="/messaging/thread/"]');
150
+ if (parentLink) {
151
+ const href = parentLink.getAttribute('href');
152
+ if (href) {
153
+ const match = href.match(/\/messaging\/thread\/([^/]+)/);
154
+ if (match)
155
+ return match[1];
156
+ }
157
+ }
158
+ // Strategy 6: Extract from element ID (conversation-card-ember50 -> ember50)
159
+ const elemId = element.id;
160
+ if (elemId && elemId.includes('conversation-card')) {
161
+ // Use the ember ID as a fallback
162
+ return elemId.replace('conversation-card-', '');
163
+ }
164
+ return null;
165
+ });
166
+ if (!threadId) {
167
+ return null;
168
+ }
169
+ // Extract participant names using multiple selector strategies
170
+ const participantNames = await this.extractTextArrayFromElement(item, [
171
+ '.msg-conversation-card__participant-names',
172
+ '.msg-conversation-listitem__participant-names',
173
+ '[class*="participant-name"]',
174
+ '[class*="participant"]',
175
+ ]);
176
+ // Fallback: get participant name from image alt attribute
177
+ if (participantNames.length === 0) {
178
+ const imgAlt = await item.evaluate((el) => {
179
+ const img = el.querySelector('img[alt]');
180
+ return img?.getAttribute('alt') || '';
181
+ });
182
+ if (imgAlt) {
183
+ participantNames.push(imgAlt);
184
+ }
185
+ }
186
+ // Extract last message preview
187
+ const lastMessageText = await this.extractTextFromElementSingle(item, [
188
+ '.msg-conversation-card__message-snippet',
189
+ '.msg-conversation-card__message-preview',
190
+ '.msg-conversation-listitem__message-preview',
191
+ '[class*="message-snippet"]',
192
+ '[class*="message-preview"]',
193
+ '[class*="last-message"]',
194
+ ]);
195
+ // Check for unread indicator using multiple strategies
196
+ const hasUnread = await this.checkForUnreadIndicator(item);
197
+ // Get timestamp if available
198
+ const timestampText = await this.extractTextFromElementSingle(item, [
199
+ 'time',
200
+ '[class*="timestamp"]',
201
+ '[class*="time"]',
202
+ ]);
203
+ // Also try to get timestamp from datetime attribute
204
+ let lastActivity;
205
+ const datetimeAttr = await item.evaluate((el) => {
206
+ const timeEl = el.querySelector('time');
207
+ return timeEl?.getAttribute('datetime') || null;
208
+ });
209
+ if (datetimeAttr) {
210
+ lastActivity = new Date(datetimeAttr);
211
+ }
212
+ else if (timestampText) {
213
+ lastActivity = this.parseRelativeTimestamp(timestampText);
214
+ }
215
+ else {
216
+ lastActivity = new Date();
217
+ }
218
+ // Create partial thread (without full messages)
219
+ const thread = {
220
+ id: threadId,
221
+ participants: participantNames.length > 0
222
+ ? participantNames.map((name) => ({ name, profileUrl: undefined }))
223
+ : [{ name: 'Unknown', profileUrl: undefined }],
224
+ messages: [
225
+ {
226
+ id: `preview-${threadId}`,
227
+ threadId,
228
+ sender: { name: participantNames[0] || 'Unknown', isMe: false },
229
+ content: lastMessageText,
230
+ timestamp: lastActivity,
231
+ isRead: !hasUnread,
232
+ },
233
+ ],
234
+ lastActivity,
235
+ unreadCount: hasUnread ? 1 : 0,
236
+ };
237
+ return thread;
238
+ }
239
+ catch (error) {
240
+ console.warn('Failed to parse thread item:', error);
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Extract multiple text values from an element - returns array of strings
246
+ */
247
+ async extractTextArrayFromElement(element, selectors) {
248
+ for (const selector of selectors) {
249
+ try {
250
+ const texts = await element.$$eval(selector, (els) => els.map((el) => el.textContent?.trim()).filter(Boolean));
251
+ if (texts.length > 0)
252
+ return texts;
253
+ }
254
+ catch {
255
+ // Try next selector
256
+ }
257
+ }
258
+ return [];
259
+ }
260
+ /**
261
+ * Extract single text value from an element - returns string
262
+ */
263
+ async extractTextFromElementSingle(element, selectors) {
264
+ for (const selector of selectors) {
265
+ try {
266
+ const text = await element.$eval(selector, (el) => el.textContent?.trim() || '');
267
+ if (text)
268
+ return text;
269
+ }
270
+ catch {
271
+ // Try next selector
272
+ }
273
+ }
274
+ return '';
275
+ }
276
+ /**
277
+ * Check if a conversation item has unread messages
278
+ */
279
+ async checkForUnreadIndicator(item) {
280
+ // Check for unread indicator element
281
+ const selectors = [
282
+ '.msg-conversation-card__unread-indicator',
283
+ '.msg-conversation-listitem__unread-indicator',
284
+ '[class*="unread"]',
285
+ ];
286
+ for (const selector of selectors) {
287
+ try {
288
+ const unreadEl = await item.$(selector);
289
+ if (unreadEl)
290
+ return true;
291
+ }
292
+ catch {
293
+ // Continue
294
+ }
295
+ }
296
+ // Check for bold/heavy font weight (unread messages often have bold text)
297
+ const fontWeight = await item.evaluate((el) => {
298
+ const nameEl = el.querySelector('[class*="participant-name"]') ||
299
+ el.querySelector('[class*="conversation-card__title"]');
300
+ if (nameEl) {
301
+ const style = window.getComputedStyle(nameEl);
302
+ return parseInt(style.fontWeight) >= 600; // semibold or bold
303
+ }
304
+ return false;
305
+ });
306
+ return fontWeight;
307
+ }
308
+ /**
309
+ * Parse relative timestamps like "5m", "2h", "Yesterday"
310
+ */
311
+ parseRelativeTimestamp(text) {
312
+ const trimmed = text.trim().toLowerCase();
313
+ const now = new Date();
314
+ // Match patterns like "5m", "2h", "3d", "1w"
315
+ const relativeMatch = trimmed.match(/^(\d+)([mhdw])$/);
316
+ if (relativeMatch) {
317
+ const value = parseInt(relativeMatch[1]);
318
+ const unit = relativeMatch[2];
319
+ switch (unit) {
320
+ case 'm':
321
+ return new Date(now.getTime() - value * 60 * 1000);
322
+ case 'h':
323
+ return new Date(now.getTime() - value * 60 * 60 * 1000);
324
+ case 'd':
325
+ return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
326
+ case 'w':
327
+ return new Date(now.getTime() - value * 7 * 24 * 60 * 60 * 1000);
328
+ }
329
+ }
330
+ // Check for "Yesterday"
331
+ if (trimmed === 'yesterday') {
332
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000);
333
+ }
334
+ // Fallback to current time
335
+ return now;
336
+ }
337
+ /**
338
+ * Get full thread with all messages
339
+ */
340
+ async getThread(options) {
341
+ const { threadId, limit = 50 } = options;
342
+ // Navigate to thread
343
+ await this.page.goto(`https://www.linkedin.com/messaging/thread/${threadId}/`, {
344
+ waitUntil: 'networkidle',
345
+ timeout: 30000,
346
+ });
347
+ // Wait for messages to load
348
+ const messagesResult = await this.selectorEngine.findElement('messages', 'messageBubble', {
349
+ timeout: 10000,
350
+ });
351
+ if (!messagesResult.element) {
352
+ console.warn('Could not find message bubbles');
353
+ return null;
354
+ }
355
+ // Get all message bubbles
356
+ const messageElements = await this.page.$$(selectors_1.SELECTORS.messages.messageBubble.join(', '));
357
+ const messages = [];
358
+ for (let i = 0; i < Math.min(messageElements.length, limit); i++) {
359
+ const message = await this.parseMessage(messageElements[i], threadId);
360
+ if (message) {
361
+ messages.push(message);
362
+ }
363
+ }
364
+ // Get participant info
365
+ const participants = await this.getThreadParticipants();
366
+ return {
367
+ id: threadId,
368
+ participants,
369
+ messages,
370
+ lastActivity: messages[messages.length - 1]?.timestamp || new Date(),
371
+ unreadCount: messages.filter((m) => !m.isRead).length,
372
+ };
373
+ }
374
+ /**
375
+ * Show a thread by clicking on a conversation card (for ember IDs) or direct navigation
376
+ * Returns the thread with messages, handling both ember IDs and real thread IDs
377
+ */
378
+ async showThread(threadId, limit = 50) {
379
+ // Check if this is an ember ID (e.g., "ember50") or a real thread ID
380
+ const isEmberId = threadId.startsWith('ember');
381
+ if (isEmberId) {
382
+ // Navigate to messaging page first
383
+ await this.navigateToMessages();
384
+ // Find and click the conversation card to get the real thread ID
385
+ const realThreadId = await this.clickAndGetThreadId(threadId);
386
+ if (!realThreadId) {
387
+ console.warn(`Could not find conversation card with ID: ${threadId}`);
388
+ return null;
389
+ }
390
+ threadId = realThreadId;
391
+ }
392
+ else {
393
+ // Check if already on the correct thread URL
394
+ const currentUrl = this.page.url();
395
+ const targetUrl = `https://www.linkedin.com/messaging/thread/${threadId}/`;
396
+ if (!currentUrl.includes(threadId)) {
397
+ // Navigate directly to the thread
398
+ await this.page.goto(targetUrl, {
399
+ waitUntil: 'domcontentloaded',
400
+ timeout: 30000,
401
+ });
402
+ }
403
+ else {
404
+ console.log('Already on correct thread URL');
405
+ }
406
+ }
407
+ // Wait for messages to load
408
+ await this.page.waitForTimeout(2000);
409
+ // Scrape messages
410
+ const messages = await this.scrapeMessages(threadId, limit);
411
+ const participants = await this.getThreadParticipants();
412
+ if (messages.length === 0) {
413
+ console.warn('No messages found in thread');
414
+ return null;
415
+ }
416
+ return {
417
+ id: threadId,
418
+ participants,
419
+ messages,
420
+ lastActivity: messages[messages.length - 1]?.timestamp || new Date(),
421
+ unreadCount: messages.filter((m) => !m.isRead).length,
422
+ };
423
+ }
424
+ /**
425
+ * Click on a conversation card and extract the real thread ID from the URL
426
+ */
427
+ async clickAndGetThreadId(emberId) {
428
+ try {
429
+ // Wait for conversation list to load
430
+ await this.page.waitForTimeout(3000);
431
+ // Strategy 1: Try to find the card by ID
432
+ let card = await this.page.$(`#conversation-card-${emberId}`);
433
+ // Strategy 2: Find by index using msg-conversation-card class
434
+ if (!card) {
435
+ const listItems = await this.page.$$('[class*="msg-conversation-listitem"]');
436
+ let conversationCards = [];
437
+ for (const listItem of listItems) {
438
+ const c = await listItem.$('.msg-conversation-card');
439
+ if (c)
440
+ conversationCards.push(c);
441
+ }
442
+ // Fallback to direct class match
443
+ if (conversationCards.length === 0) {
444
+ conversationCards = (await this.page.$$('.msg-conversation-card'));
445
+ }
446
+ // The ember ID number (e.g., ember50 -> 50) doesn't match DOM position
447
+ // We need to find the card that has this ember ID in its element ID
448
+ const targetEmberNumber = emberId.replace('ember', '');
449
+ for (let i = 0; i < conversationCards.length; i++) {
450
+ const cardId = await conversationCards[i].evaluate((el) => el.id);
451
+ if (cardId && cardId.includes(targetEmberNumber)) {
452
+ card = conversationCards[i];
453
+ break;
454
+ }
455
+ }
456
+ // If still not found, try using the first card as a test
457
+ if (!card && conversationCards.length > 0) {
458
+ card = conversationCards[0];
459
+ }
460
+ }
461
+ // Strategy 3: Try data-testid
462
+ if (!card) {
463
+ const cards = await this.page.$$('[data-testid="conversation-card"]');
464
+ const index = parseInt(emberId.replace('ember', ''));
465
+ if (!isNaN(index) && cards.length > index) {
466
+ card = cards[index];
467
+ }
468
+ }
469
+ if (!card) {
470
+ return null;
471
+ }
472
+ // Click the card - LinkedIn SPA doesn't trigger navigation event
473
+ await card.click();
474
+ // Wait for URL to change (SPA navigation)
475
+ await this.page.waitForTimeout(2000);
476
+ // Extract thread ID from the new URL
477
+ const currentUrl = this.page.url();
478
+ const match = currentUrl.match(/\/messaging\/thread\/([^/]+)/);
479
+ if (match) {
480
+ return match[1];
481
+ }
482
+ return null;
483
+ }
484
+ catch (error) {
485
+ console.warn('Failed to click conversation card:', error);
486
+ return null;
487
+ }
488
+ }
489
+ /**
490
+ * Scrape messages from a loaded thread page
491
+ */
492
+ async scrapeMessages(threadId, limit = 50) {
493
+ const messages = [];
494
+ // Wait for messages to load
495
+ await this.page.waitForTimeout(3000);
496
+ // Look for actual message content in the conversation
497
+ // LinkedIn uses event list items for messages
498
+ const messageElements = (await this.page.$$('.msg-s-event-listitem'));
499
+ for (const msgEl of messageElements) {
500
+ if (messages.length >= limit)
501
+ break;
502
+ // Get sender name
503
+ const senderName = await msgEl
504
+ .$eval('.msg-s-message-group__name', (el) => el.textContent?.trim() || '')
505
+ .catch(() => '');
506
+ // Get timestamp
507
+ let timestamp = new Date();
508
+ const datetimeAttr = await msgEl
509
+ .$eval('time', (el) => el.getAttribute('datetime'))
510
+ .catch(() => null);
511
+ if (datetimeAttr) {
512
+ timestamp = new Date(datetimeAttr);
513
+ }
514
+ // Get message content
515
+ const content = await msgEl
516
+ .$eval('.msg-s-event-listitem__body, [class*="event-listitem__body"]', (el) => el.textContent?.trim() || '')
517
+ .catch(() => '');
518
+ // Skip if no content
519
+ if (!content || content.length < 2)
520
+ continue;
521
+ // Skip UI noise
522
+ if (content.includes('Open Emoji Keyboard') ||
523
+ content.includes('Maximize compose') ||
524
+ content.includes('Send to') ||
525
+ content === 'Download') {
526
+ continue;
527
+ }
528
+ // Check if from current user
529
+ const isMe = await msgEl.evaluate((el) => {
530
+ return (el.classList.contains('msg-s-message-group--me') ||
531
+ el.classList.contains('sent') ||
532
+ el.closest('.msg-s-message-group--me') !== null);
533
+ });
534
+ messages.push({
535
+ id: `msg-${threadId}-${messages.length}`,
536
+ threadId,
537
+ sender: {
538
+ name: senderName || (isMe ? 'You' : 'Unknown'),
539
+ isMe,
540
+ },
541
+ content,
542
+ timestamp,
543
+ isRead: true,
544
+ });
545
+ }
546
+ // If no messages found with event list items, try alternative approach
547
+ if (messages.length === 0) {
548
+ const messageContainers = (await this.page.$$('[class*="message"]'));
549
+ const seenContent = new Set();
550
+ for (const container of messageContainers) {
551
+ if (messages.length >= limit)
552
+ break;
553
+ const content = await container.evaluate((el) => {
554
+ // Skip meta elements
555
+ if (el.classList.contains('msg-s-message-group__meta') ||
556
+ el.classList.contains('msg-s-message-group__name') ||
557
+ el.classList.contains('msg-s-message-group__timestamp') ||
558
+ el.tagName.toLowerCase() === 'time') {
559
+ return '';
560
+ }
561
+ const text = el.textContent?.trim() || '';
562
+ // Skip short or UI text
563
+ if (text.length < 5 ||
564
+ text.includes('Open Emoji Keyboard') ||
565
+ text.includes('Download') ||
566
+ text.includes('MB') ||
567
+ text.includes('AM') ||
568
+ text.includes('PM')) {
569
+ return '';
570
+ }
571
+ return text;
572
+ });
573
+ if (content && !seenContent.has(content)) {
574
+ seenContent.add(content);
575
+ const isMe = await container.evaluate((el) => {
576
+ return el.closest('.msg-s-message-group--me') !== null;
577
+ });
578
+ messages.push({
579
+ id: `msg-${threadId}-${messages.length}`,
580
+ threadId,
581
+ sender: {
582
+ name: isMe ? 'You' : 'Other',
583
+ isMe,
584
+ },
585
+ content,
586
+ timestamp: new Date(),
587
+ isRead: true,
588
+ });
589
+ }
590
+ }
591
+ }
592
+ return messages;
593
+ }
594
+ /**
595
+ * Format thread messages as human-readable text (Style A)
596
+ */
597
+ formatThreadText(thread) {
598
+ const lines = [];
599
+ const participantNames = thread.participants.map((p) => p.name).join(', ');
600
+ lines.push(`Conversations with ${participantNames}`);
601
+ lines.push('─'.repeat(60));
602
+ let lastDate = '';
603
+ for (const message of thread.messages) {
604
+ const date = message.timestamp.toLocaleDateString();
605
+ if (date !== lastDate) {
606
+ lines.push('');
607
+ lines.push(`─── ${date} ───`);
608
+ lastDate = date;
609
+ }
610
+ const time = message.timestamp.toLocaleTimeString([], {
611
+ hour: '2-digit',
612
+ minute: '2-digit',
613
+ });
614
+ const sender = message.sender.isMe ? 'You' : message.sender.name;
615
+ lines.push(`${time} ${sender}: ${message.content}`);
616
+ }
617
+ return lines.join('\n');
618
+ }
619
+ /**
620
+ * Format thread messages as JSON for LLM processing
621
+ */
622
+ formatThreadJson(thread) {
623
+ return JSON.stringify({
624
+ threadId: thread.id,
625
+ participants: thread.participants,
626
+ messages: thread.messages.map((m) => ({
627
+ id: m.id,
628
+ sender: {
629
+ name: m.sender.name,
630
+ isMe: m.sender.isMe,
631
+ },
632
+ content: m.content,
633
+ timestamp: m.timestamp.toISOString(),
634
+ })),
635
+ }, null, 2);
636
+ }
637
+ /**
638
+ * Parse a single message element
639
+ */
640
+ async parseMessage(element, threadId) {
641
+ try {
642
+ // Extract message ID
643
+ const messageId = await element.evaluate((el) => {
644
+ const element = el;
645
+ return (element.getAttribute('data-urn') || element.id || `msg-${Date.now()}-${Math.random()}`);
646
+ });
647
+ // Extract sender info - look for the sender name element
648
+ const senderInfo = await element
649
+ .$eval('.msg-s-message-group__name', (el) => ({
650
+ name: el.textContent?.trim() || '',
651
+ profileUrl: el.getAttribute('href') || undefined,
652
+ }))
653
+ .catch(() => ({ name: '', profileUrl: undefined }));
654
+ // Extract message content - look for the message body
655
+ let content = await element
656
+ .$eval('.msg-s-event-listitem__body', (el) => el.textContent?.trim() || '')
657
+ .catch(() => '');
658
+ // Fallback: try other content selectors
659
+ if (!content) {
660
+ content = await element
661
+ .$eval('.msg-s-message-group__message-body', (el) => el.textContent?.trim() || '')
662
+ .catch(() => '');
663
+ }
664
+ // Fallback: try any element that looks like message content
665
+ if (!content) {
666
+ const contentEl = await element.$('[class*="message-body"]');
667
+ if (contentEl) {
668
+ content = await contentEl.evaluate((el) => el.textContent?.trim() || '');
669
+ }
670
+ }
671
+ // Skip if no meaningful content
672
+ if (!content || content.length < 2) {
673
+ return null;
674
+ }
675
+ // Extract timestamp
676
+ let timestamp = new Date();
677
+ const datetimeAttr = await element
678
+ .$eval('time', (el) => el.getAttribute('datetime'))
679
+ .catch(() => null);
680
+ if (datetimeAttr) {
681
+ timestamp = new Date(datetimeAttr);
682
+ }
683
+ else {
684
+ // Try to get timestamp from text content
685
+ const timeText = await element
686
+ .$eval('.msg-s-message-group__timestamp', (el) => el.textContent?.trim() || '')
687
+ .catch(() => '');
688
+ if (timeText) {
689
+ // Try to parse relative time
690
+ const now = new Date();
691
+ if (timeText.toLowerCase().includes('today')) {
692
+ timestamp = now;
693
+ }
694
+ else if (timeText.toLowerCase().includes('yesterday')) {
695
+ timestamp = new Date(now.getTime() - 24 * 60 * 60 * 1000);
696
+ }
697
+ }
698
+ }
699
+ // Check if message is from current user
700
+ const isMe = await element.evaluate((el) => {
701
+ const element = el;
702
+ return (element.classList.contains('msg-s-message-group--me') ||
703
+ element.classList.contains('sent') ||
704
+ element.closest('.msg-s-message-group--me') !== null);
705
+ });
706
+ return {
707
+ id: messageId,
708
+ threadId,
709
+ sender: {
710
+ name: senderInfo.name || (isMe ? 'You' : 'Unknown'),
711
+ profileUrl: senderInfo.profileUrl,
712
+ isMe,
713
+ },
714
+ content,
715
+ timestamp,
716
+ isRead: true,
717
+ };
718
+ }
719
+ catch (error) {
720
+ console.warn('Failed to parse message:', error);
721
+ return null;
722
+ }
723
+ }
724
+ /**
725
+ * Get thread participant information
726
+ */
727
+ async getThreadParticipants() {
728
+ try {
729
+ // Look for participant names in the thread header
730
+ const participants = await this.page.$$eval('.msg-entity-lockup__entity-title, [class*="participant-name"], [class*="thread-title"]', (elements) => elements.map((el) => ({
731
+ name: el.textContent?.trim() || 'Unknown',
732
+ profileUrl: el.getAttribute('href') || undefined,
733
+ })));
734
+ if (participants.length > 0) {
735
+ return participants;
736
+ }
737
+ }
738
+ catch {
739
+ // Fall through to default
740
+ }
741
+ return [{ name: 'Unknown', profileUrl: undefined }];
742
+ }
743
+ }
744
+ exports.LinkedInMessages = LinkedInMessages;
745
+ //# sourceMappingURL=messages.js.map