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,437 @@
1
+ /**
2
+ * DOM Extractor Module
3
+ *
4
+ * Extracts semantic DOM representation from LinkedIn pages using Playwright.
5
+ * Provides structured data extraction for AI agent action planning.
6
+ */
7
+
8
+ import type { Page } from 'playwright';
9
+ import type {
10
+ DOMElement,
11
+ DOMRepresentation,
12
+ PageMetadata,
13
+ PageType,
14
+ ConnectionState,
15
+ } from './types';
16
+
17
+ /**
18
+ * Extracts DOM representation from a Playwright page
19
+ */
20
+ export class DOMExtractor {
21
+ /**
22
+ * Extract complete DOM representation from the page
23
+ */
24
+ async extract(page: Page): Promise<DOMRepresentation> {
25
+ const url = page.url();
26
+ const title = await page.title();
27
+
28
+ // Extract elements and metadata in parallel
29
+ const [elements, metadata] = await Promise.all([
30
+ this.extractElements(page),
31
+ this.extractMetadata(page),
32
+ ]);
33
+
34
+ return {
35
+ url,
36
+ title,
37
+ elements,
38
+ metadata,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Extract interactive elements from the page
44
+ */
45
+ private async extractElements(page: Page): Promise<DOMElement[]> {
46
+ return page.evaluate(() => {
47
+ const elements: DOMElement[] = [];
48
+ const seenElements = new Set<Element>();
49
+
50
+ // Define selectors for interactive elements
51
+ const selectors = [
52
+ 'button',
53
+ 'a[href]',
54
+ 'input',
55
+ 'textarea',
56
+ 'select',
57
+ '[role="button"]',
58
+ '[role="link"]',
59
+ '[role="textbox"]',
60
+ '[role="checkbox"]',
61
+ '[role="radio"]',
62
+ '[role="combobox"]',
63
+ '[role="menuitem"]',
64
+ '[role="tab"]',
65
+ '[contenteditable="true"]',
66
+ ];
67
+
68
+ // Find all matching elements
69
+ const candidates = document.querySelectorAll(selectors.join(', '));
70
+
71
+ candidates.forEach((element, index) => {
72
+ // Skip duplicates
73
+ if (seenElements.has(element)) return;
74
+ seenElements.add(element);
75
+
76
+ // Check visibility
77
+ if (!isElementVisible(element)) return;
78
+
79
+ const extracted = extractElementData(element, index);
80
+ if (extracted) {
81
+ elements.push(extracted);
82
+ }
83
+ });
84
+
85
+ return elements;
86
+
87
+ /**
88
+ * Check if element is visible
89
+ */
90
+ function isElementVisible(element: Element): boolean {
91
+ const style = window.getComputedStyle(element);
92
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
93
+ return false;
94
+ }
95
+
96
+ // Check if element has dimensions
97
+ const rect = element.getBoundingClientRect();
98
+ if (rect.width === 0 || rect.height === 0) {
99
+ return false;
100
+ }
101
+
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Extract data from a single element
107
+ */
108
+ function extractElementData(element: Element, index: number): DOMElement | null {
109
+ const rect = element.getBoundingClientRect();
110
+ const htmlElement = element as HTMLElement;
111
+
112
+ // Get basic properties
113
+ const tag = element.tagName.toLowerCase();
114
+ const text = getElementText(element);
115
+ const ariaLabel = element.getAttribute('aria-label') || undefined;
116
+ const role = element.getAttribute('role') || undefined;
117
+
118
+ // Get input-specific properties
119
+ let type: string | undefined;
120
+ let value: string | undefined;
121
+
122
+ // eslint-disable-next-line no-undef
123
+ if (element instanceof HTMLInputElement) {
124
+ type = (element as unknown as { type: string }).type;
125
+ value = (element as unknown as { value: string }).value || undefined;
126
+ // eslint-disable-next-line no-undef
127
+ } else if (element instanceof HTMLSelectElement) {
128
+ value = (element as unknown as { value: string }).value || undefined;
129
+ // eslint-disable-next-line no-undef
130
+ } else if (element instanceof HTMLTextAreaElement) {
131
+ type = 'textarea';
132
+ value = (element as unknown as { value: string }).value || undefined;
133
+ }
134
+
135
+ // Check if enabled
136
+ const enabled = !(
137
+ 'disabled' in htmlElement &&
138
+ (htmlElement as unknown as { disabled?: boolean }).disabled === true
139
+ );
140
+
141
+ // Generate ID
142
+ const id = generateElementId(tag, text || ariaLabel || '', index);
143
+
144
+ return {
145
+ id,
146
+ tag,
147
+ role,
148
+ ariaLabel,
149
+ text: truncateText(text, 200),
150
+ type,
151
+ value,
152
+ visible: true,
153
+ enabled,
154
+ bbox: {
155
+ x: Math.round(rect.x),
156
+ y: Math.round(rect.y),
157
+ width: Math.round(rect.width),
158
+ height: Math.round(rect.height),
159
+ },
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Get visible text content of an element
165
+ */
166
+ function getElementText(element: Element): string {
167
+ // For inputs, use placeholder or value
168
+ // eslint-disable-next-line no-undef
169
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
170
+ return (
171
+ (element as unknown as { placeholder?: string }).placeholder ||
172
+ (element as unknown as { value?: string }).value ||
173
+ ''
174
+ );
175
+ }
176
+
177
+ // For links, get text content
178
+ const text = element.textContent?.trim() || '';
179
+
180
+ // For buttons, get aria-label if no text
181
+ if (!text && element.getAttribute('aria-label')) {
182
+ return element.getAttribute('aria-label')!;
183
+ }
184
+
185
+ return text;
186
+ }
187
+
188
+ /**
189
+ * Truncate text to max length
190
+ */
191
+ function truncateText(text: string, maxLength: number): string {
192
+ if (!text) return '';
193
+ if (text.length <= maxLength) return text;
194
+ return text.substring(0, maxLength) + '...';
195
+ }
196
+
197
+ /**
198
+ * Generate unique element ID
199
+ * Format: elem-{tag}-{label}-{index}
200
+ */
201
+ function generateElementId(tag: string, label: string, index: number): string {
202
+ const cleanLabel = label
203
+ .toLowerCase()
204
+ .replace(/[^a-z0-9]+/g, '-')
205
+ .replace(/^-+|-+$/g, '')
206
+ .substring(0, 30);
207
+
208
+ if (cleanLabel) {
209
+ return `elem-${tag}-${cleanLabel}-${index}`;
210
+ }
211
+ return `elem-${tag}-${index}`;
212
+ }
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Extract metadata from the page
218
+ */
219
+ private async extractMetadata(page: Page): Promise<PageMetadata> {
220
+ return page.evaluate(() => {
221
+ const url = window.location.href;
222
+ const pageType = detectPageType(url);
223
+ const profileName = extractProfileName();
224
+ const profileTitle = extractProfileTitle();
225
+ const connectionState = detectConnectionState();
226
+ const statusMessages = extractStatusMessages();
227
+
228
+ return {
229
+ pageType,
230
+ profileName,
231
+ profileTitle,
232
+ connectionState,
233
+ statusMessages,
234
+ };
235
+
236
+ /**
237
+ * Detect the type of LinkedIn page
238
+ */
239
+ function detectPageType(url: string): PageType {
240
+ if (url.includes('/in/')) return 'profile';
241
+ if (url.includes('/messaging')) return 'messaging';
242
+ if (url.includes('/feed')) return 'feed';
243
+ if (url.includes('/search')) return 'search';
244
+ if (url.includes('/notifications')) return 'notifications';
245
+ return 'unknown';
246
+ }
247
+
248
+ /**
249
+ * Extract profile name from page
250
+ */
251
+ function extractProfileName(): string | undefined {
252
+ // Try h1 first
253
+ const h1 = document.querySelector('h1');
254
+ if (h1) {
255
+ const text = h1.textContent?.trim();
256
+ if (text) return text;
257
+ }
258
+
259
+ // Try data-testid attributes common in LinkedIn
260
+ const selectors = [
261
+ '[data-testid="profile-name"]',
262
+ '[data-testid="top-card-profile-name"]',
263
+ '.profile-name',
264
+ '.top-card-layout__title',
265
+ 'h1.text-heading-xlarge',
266
+ ];
267
+
268
+ for (const selector of selectors) {
269
+ const element = document.querySelector(selector);
270
+ if (element) {
271
+ const text = element.textContent?.trim();
272
+ if (text) return text;
273
+ }
274
+ }
275
+
276
+ return undefined;
277
+ }
278
+
279
+ /**
280
+ * Extract profile title/headline
281
+ */
282
+ function extractProfileTitle(): string | undefined {
283
+ const selectors = [
284
+ '[data-testid="profile-headline"]',
285
+ '[data-testid="top-card-profile-headline"]',
286
+ '.profile-headline',
287
+ '.top-card-layout__headline',
288
+ '.text-body-medium',
289
+ ];
290
+
291
+ for (const selector of selectors) {
292
+ const element = document.querySelector(selector);
293
+ if (element) {
294
+ const text = element.textContent?.trim();
295
+ if (text) return text;
296
+ }
297
+ }
298
+
299
+ return undefined;
300
+ }
301
+
302
+ /**
303
+ * Detect connection state from page
304
+ */
305
+ function detectConnectionState(): ConnectionState {
306
+ // Look for connection indicators
307
+ const pageText = document.body.innerText.toLowerCase();
308
+
309
+ // Check for "Connect" button text
310
+ const hasConnectButton = document.querySelector(
311
+ 'button[aria-label*="connect" i], button:contains("Connect")'
312
+ );
313
+
314
+ // Check for pending status
315
+ const hasPendingText = pageText.includes('pending') || pageText.includes('invitation sent');
316
+
317
+ // Check for "Message" button (indicates connected)
318
+ const hasMessageButton = document.querySelector(
319
+ 'button[aria-label*="message" i], button[data-control-name="message"], a[href*="messaging"]:not([href*="invite"])'
320
+ );
321
+
322
+ // Check for "Connected" text or similar indicators
323
+ const hasConnectedText =
324
+ pageText.includes('connected') ||
325
+ pageText.includes('1st') ||
326
+ pageText.includes('1st degree');
327
+
328
+ if (hasConnectedText || hasMessageButton) {
329
+ return 'connected';
330
+ }
331
+
332
+ if (hasPendingText) {
333
+ return 'pending';
334
+ }
335
+
336
+ if (hasConnectButton) {
337
+ return 'not_connected';
338
+ }
339
+
340
+ return 'unknown';
341
+ }
342
+
343
+ /**
344
+ * Extract status messages/notifications visible on page
345
+ */
346
+ function extractStatusMessages(): string[] {
347
+ const messages: string[] = [];
348
+
349
+ // Look for common status message containers
350
+ const selectors = [
351
+ '[data-testid="status-message"]',
352
+ '.artdeco-toast-item',
353
+ '.alert',
354
+ '.notification-badge',
355
+ '[role="alert"]',
356
+ '[role="status"]',
357
+ '.feed-shared-update-v2__description',
358
+ '.artdeco-inline-feedback',
359
+ ];
360
+
361
+ for (const selector of selectors) {
362
+ const elements = document.querySelectorAll(selector);
363
+ elements.forEach((el) => {
364
+ const text = el.textContent?.trim();
365
+ if (text && text.length > 0 && text.length < 500) {
366
+ messages.push(text);
367
+ }
368
+ });
369
+ }
370
+
371
+ // Deduplicate
372
+ return [...new Set(messages)];
373
+ }
374
+ });
375
+ }
376
+
377
+ /**
378
+ * Find element by ID in the extracted DOM representation
379
+ */
380
+ findElementById(dom: DOMRepresentation, elementId: string): DOMElement | undefined {
381
+ return dom.elements.find((el) => el.id === elementId);
382
+ }
383
+
384
+ /**
385
+ * Find elements by tag name
386
+ */
387
+ findElementsByTag(dom: DOMRepresentation, tag: string): DOMElement[] {
388
+ return dom.elements.filter((el) => el.tag === tag.toLowerCase());
389
+ }
390
+
391
+ /**
392
+ * Find elements by text content (partial match)
393
+ */
394
+ findElementsByText(dom: DOMRepresentation, text: string): DOMElement[] {
395
+ const searchText = text.toLowerCase();
396
+ return dom.elements.filter(
397
+ (el) =>
398
+ el.text.toLowerCase().includes(searchText) ||
399
+ (el.ariaLabel && el.ariaLabel.toLowerCase().includes(searchText))
400
+ );
401
+ }
402
+
403
+ /**
404
+ * Get clickable elements (buttons and links)
405
+ */
406
+ getClickableElements(dom: DOMRepresentation): DOMElement[] {
407
+ return dom.elements.filter(
408
+ (el) =>
409
+ (el.tag === 'button' || el.tag === 'a' || el.role === 'button' || el.role === 'link') &&
410
+ el.visible &&
411
+ el.enabled
412
+ );
413
+ }
414
+
415
+ /**
416
+ * Get input elements (inputs, textareas, selects)
417
+ */
418
+ getInputElements(dom: DOMRepresentation): DOMElement[] {
419
+ return dom.elements.filter(
420
+ (el) =>
421
+ (el.tag === 'input' ||
422
+ el.tag === 'textarea' ||
423
+ el.tag === 'select' ||
424
+ el.role === 'textbox' ||
425
+ el.role === 'combobox') &&
426
+ el.visible &&
427
+ el.enabled
428
+ );
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Create a new DOMExtractor instance
434
+ */
435
+ export function createDOMExtractor(): DOMExtractor {
436
+ return new DOMExtractor();
437
+ }