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,393 @@
1
+ import { Page, ElementHandle } from 'playwright';
2
+ import { SELECTORS, SelectorCategory } from './selectors';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+
7
+ export interface SelectorResult {
8
+ element: ElementHandle | null;
9
+ selector: string | null;
10
+ attempts: string[];
11
+ method: string;
12
+ }
13
+
14
+ export interface SelectorLearning {
15
+ successes: Record<string, number>;
16
+ failures: Record<string, number>;
17
+ }
18
+
19
+ export class SelectorEngine {
20
+ private page: Page;
21
+ private learningPath: string;
22
+ private learning: SelectorLearning;
23
+
24
+ constructor(page: Page) {
25
+ this.page = page;
26
+ this.learningPath = path.join(os.homedir(), '.linkedin-cli', 'selector-learning.json');
27
+ this.learning = this.loadLearning();
28
+ }
29
+
30
+ private loadLearning(): SelectorLearning {
31
+ try {
32
+ if (fs.existsSync(this.learningPath)) {
33
+ const data = fs.readFileSync(this.learningPath, 'utf-8');
34
+ return JSON.parse(data);
35
+ }
36
+ } catch (error) {
37
+ console.log('Could not load selector learning:', error);
38
+ }
39
+ return { successes: {}, failures: {} };
40
+ }
41
+
42
+ private saveLearning(): void {
43
+ try {
44
+ const dir = path.dirname(this.learningPath);
45
+ if (!fs.existsSync(dir)) {
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ }
48
+ fs.writeFileSync(this.learningPath, JSON.stringify(this.learning, null, 2));
49
+ } catch (error) {
50
+ console.log('Could not save selector learning:', error);
51
+ }
52
+ }
53
+
54
+ private recordSuccess(key: string): void {
55
+ this.learning.successes[key] = (this.learning.successes[key] || 0) + 1;
56
+ this.saveLearning();
57
+ }
58
+
59
+ private recordFailure(key: string): void {
60
+ this.learning.failures[key] = (this.learning.failures[key] || 0) + 1;
61
+ this.saveLearning();
62
+ }
63
+
64
+ /**
65
+ * Find element using hybrid approach:
66
+ * 1. Accessibility locators (getByRole, getByLabel, getByTestId)
67
+ * 2. Text pattern matching
68
+ * 3. CSS selectors with fuzzy matching
69
+ * 4. Legacy selectors from SELECTORS
70
+ */
71
+ async findElement(
72
+ category: SelectorCategory,
73
+ key: string,
74
+ options: { timeout?: number; visible?: boolean; text?: string } = {}
75
+ ): Promise<SelectorResult> {
76
+ const attempts: string[] = [];
77
+ const learningKey = `${category}.${key}`;
78
+
79
+ // Layer 1: Accessibility-first queries using Playwright locators
80
+ const a11yResult = await this.findByAccessibility(category, key, options);
81
+ if (a11yResult.element) {
82
+ this.recordSuccess(learningKey);
83
+ return a11yResult;
84
+ }
85
+ attempts.push(...a11yResult.attempts);
86
+
87
+ // Layer 2: Text pattern matching
88
+ if (options.text) {
89
+ const textResult = await this.findByText(options.text, options.timeout);
90
+ if (textResult.element) {
91
+ this.recordSuccess(learningKey);
92
+ return textResult;
93
+ }
94
+ attempts.push(...textResult.attempts);
95
+ }
96
+
97
+ // Layer 3: Pattern-based CSS selectors (fuzzy class matching)
98
+ const patternResult = await this.findByPattern(category, key, options);
99
+ if (patternResult.element) {
100
+ this.recordSuccess(learningKey);
101
+ return patternResult;
102
+ }
103
+ attempts.push(...patternResult.attempts);
104
+
105
+ // Layer 4: Legacy selectors from SELECTORS
106
+ const legacyResult = await this.findByLegacy(category, key, options);
107
+ if (legacyResult.element) {
108
+ this.recordSuccess(learningKey);
109
+ return legacyResult;
110
+ }
111
+ attempts.push(...legacyResult.attempts);
112
+
113
+ // No element found
114
+ this.recordFailure(learningKey);
115
+ return { element: null, selector: null, attempts, method: 'none' };
116
+ }
117
+
118
+ /**
119
+ * Layer 1: Accessibility-first queries
120
+ */
121
+ private async findByAccessibility(
122
+ category: SelectorCategory,
123
+ key: string,
124
+ options: { timeout?: number } = {}
125
+ ): Promise<SelectorResult> {
126
+ const timeout = options.timeout ?? 5000;
127
+ const attempts: string[] = [];
128
+
129
+ // Define accessibility queries based on category/key
130
+ const queries = this.buildAccessibilityQueries(category, key);
131
+
132
+ for (const query of queries) {
133
+ try {
134
+ attempts.push(`getByRole(${query.role}${query.name ? `, "${query.name}"` : ''})`);
135
+ const element = await this.page
136
+ .getByRole(query.role as any, {
137
+ name: query.name ? new RegExp(query.name, 'i') : undefined,
138
+ })
139
+ .elementHandle({ timeout });
140
+ if (element) {
141
+ return { element, selector: attempts[attempts.length - 1], attempts, method: 'a11y' };
142
+ }
143
+ } catch {
144
+ // Continue to next query
145
+ }
146
+
147
+ // Try getByLabel
148
+ if (query.label) {
149
+ try {
150
+ attempts.push(`getByLabel("${query.label}")`);
151
+ const element = await this.page
152
+ .getByLabel(new RegExp(query.label, 'i'))
153
+ .elementHandle({ timeout });
154
+ if (element) {
155
+ return { element, selector: attempts[attempts.length - 1], attempts, method: 'a11y' };
156
+ }
157
+ } catch {
158
+ // Continue
159
+ }
160
+ }
161
+
162
+ // Try getByTestId
163
+ if (query.testId) {
164
+ try {
165
+ attempts.push(`getByTestId("${query.testId}")`);
166
+ const element = await this.page.getByTestId(query.testId).elementHandle({ timeout });
167
+ if (element) {
168
+ return { element, selector: attempts[attempts.length - 1], attempts, method: 'a11y' };
169
+ }
170
+ } catch {
171
+ // Continue
172
+ }
173
+ }
174
+ }
175
+
176
+ return { element: null, selector: null, attempts, method: 'a11y' };
177
+ }
178
+
179
+ private buildAccessibilityQueries(
180
+ category: SelectorCategory,
181
+ key: string
182
+ ): Array<{
183
+ role?: string;
184
+ name?: string;
185
+ label?: string;
186
+ testId?: string;
187
+ }> {
188
+ // Map category+key to accessibility queries
189
+ const queries: Array<{ role?: string; name?: string; label?: string; testId?: string }> = [];
190
+
191
+ if (category === 'messages' && key === 'conversationList') {
192
+ queries.push(
193
+ { role: 'list', name: 'conversation' },
194
+ { role: 'list', name: 'message' },
195
+ { role: 'region', name: 'conversations' },
196
+ { testId: 'conversations-list' },
197
+ { testId: 'conversation-list' },
198
+ { label: 'conversations' },
199
+ { label: 'messages' }
200
+ );
201
+ }
202
+
203
+ if (category === 'messages' && key === 'conversationItem') {
204
+ queries.push(
205
+ { role: 'listitem' },
206
+ { role: 'article' },
207
+ { role: 'button', name: 'conversation' },
208
+ { testId: 'conversation-card' }
209
+ );
210
+ }
211
+
212
+ if (category === 'messages' && key === 'messageInput') {
213
+ queries.push(
214
+ { role: 'textbox', name: 'message' },
215
+ { role: 'textbox', name: 'Write a message' },
216
+ { label: 'Write a message' },
217
+ { testId: 'message-input' }
218
+ );
219
+ }
220
+
221
+ return queries;
222
+ }
223
+
224
+ /**
225
+ * Layer 2: Find by text content
226
+ */
227
+ private async findByText(text: string, timeout?: number): Promise<SelectorResult> {
228
+ const attempts: string[] = [];
229
+
230
+ try {
231
+ attempts.push(`getByText("${text}")`);
232
+ const element = await this.page
233
+ .getByText(new RegExp(text, 'i'))
234
+ .elementHandle({ timeout: timeout ?? 5000 });
235
+ if (element) {
236
+ return { element, selector: attempts[attempts.length - 1], attempts, method: 'text' };
237
+ }
238
+ } catch {
239
+ // Continue
240
+ }
241
+
242
+ return { element: null, selector: null, attempts, method: 'text' };
243
+ }
244
+
245
+ /**
246
+ * Layer 3: Pattern-based CSS selectors
247
+ */
248
+ private async findByPattern(
249
+ category: SelectorCategory,
250
+ key: string,
251
+ options: { timeout?: number } = {}
252
+ ): Promise<SelectorResult> {
253
+ const timeout = options.timeout ?? 5000;
254
+ const attempts: string[] = [];
255
+
256
+ // Get patterns for this category/key
257
+ const patterns = this.getPatternSelectors(category, key);
258
+
259
+ for (const pattern of patterns) {
260
+ try {
261
+ attempts.push(`CSS: ${pattern}`);
262
+ const element = await this.page.waitForSelector(pattern, { timeout, state: 'attached' });
263
+ if (element) {
264
+ return { element, selector: pattern, attempts, method: 'pattern' };
265
+ }
266
+ } catch {
267
+ // Continue
268
+ }
269
+ }
270
+
271
+ return { element: null, selector: null, attempts, method: 'pattern' };
272
+ }
273
+
274
+ private getPatternSelectors(category: SelectorCategory, key: string): string[] {
275
+ // Pattern-based selectors using common LinkedIn class patterns
276
+ const patterns: Record<string, Record<string, string[]>> = {
277
+ messages: {
278
+ conversationList: [
279
+ '[class*="msg-conversations"]',
280
+ '[class*="conversations-list"]',
281
+ '[class*="msg-options"]',
282
+ 'div[role="list"]',
283
+ 'ul[role="list"]',
284
+ ],
285
+ conversationItem: [
286
+ '[class*="msg-conversation"]',
287
+ '[class*="conversation-card"]',
288
+ '[class*="message-card"]',
289
+ 'li[role="listitem"]',
290
+ 'div[role="listitem"]',
291
+ ],
292
+ messageInput: [
293
+ '[class*="msg-form"]',
294
+ '[class*="message-input"]',
295
+ '[contenteditable="true"]',
296
+ 'div[role="textbox"]',
297
+ ],
298
+ },
299
+ };
300
+
301
+ return (patterns[category] as any)?.[key] || [];
302
+ }
303
+
304
+ /**
305
+ * Layer 4: Legacy selectors from SELECTORS constant
306
+ */
307
+ private async findByLegacy(
308
+ category: SelectorCategory,
309
+ key: string,
310
+ options: { timeout?: number; visible?: boolean } = {}
311
+ ): Promise<SelectorResult> {
312
+ const selectors = this.getSelectors(category, key);
313
+ const attempts: string[] = [];
314
+
315
+ for (const selector of selectors) {
316
+ attempts.push(`Legacy: ${selector}`);
317
+ try {
318
+ const element = await this.page.waitForSelector(selector, {
319
+ timeout: options.timeout ?? 5000,
320
+ state: options.visible ? 'visible' : 'attached',
321
+ });
322
+ if (element) {
323
+ return { element, selector, attempts, method: 'legacy' };
324
+ }
325
+ } catch {
326
+ // Continue to next selector
327
+ }
328
+ }
329
+
330
+ return { element: null, selector: null, attempts, method: 'legacy' };
331
+ }
332
+
333
+ /**
334
+ * Get selector strings for a category/key
335
+ */
336
+ private getSelectors(category: SelectorCategory, key: string): string[] {
337
+ const categorySelectors = SELECTORS[category];
338
+ if (!categorySelectors) return [];
339
+
340
+ const keySelectors = categorySelectors[key as keyof typeof categorySelectors];
341
+ if (!keySelectors) return [];
342
+
343
+ return keySelectors as string[];
344
+ }
345
+
346
+ /**
347
+ * Quickly check if an element exists without waiting
348
+ */
349
+ async hasElement(category: SelectorCategory, key: string): Promise<boolean> {
350
+ const result = await this.findElement(category, key, { timeout: 2000 });
351
+ if (result.element) {
352
+ await result.element.dispose();
353
+ return true;
354
+ }
355
+ return false;
356
+ }
357
+
358
+ /**
359
+ * Try to find element using semantic/accessible attributes first
360
+ */
361
+ async findAccessible(
362
+ options: {
363
+ role?: string;
364
+ name?: string;
365
+ label?: string;
366
+ },
367
+ timeout = 5000
368
+ ): Promise<ElementHandle | null> {
369
+ const selectors: string[] = [];
370
+
371
+ if (options.role && options.name) {
372
+ selectors.push(`[role="${options.role}"][aria-label*="${options.name}"]`);
373
+ selectors.push(`[role="${options.role}"][title*="${options.name}"]`);
374
+ }
375
+ if (options.label) {
376
+ selectors.push(`[aria-label*="${options.label}"]`);
377
+ selectors.push(`[placeholder*="${options.label}"]`);
378
+ }
379
+ if (options.role) {
380
+ selectors.push(`[role="${options.role}"]`);
381
+ }
382
+
383
+ for (const selector of selectors) {
384
+ try {
385
+ const element = await this.page.waitForSelector(selector, { timeout });
386
+ if (element) return element;
387
+ } catch {
388
+ continue;
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Multi-layer selector system for LinkedIn's unstable UI
3
+ * Each selector key has multiple fallback strategies
4
+ */
5
+ export const SELECTORS = {
6
+ // Login page
7
+ login: {
8
+ emailInput: ['input#username', 'input[name="session_key"]', 'input[type="email"]'],
9
+ passwordInput: ['input#password', 'input[name="session_password"]', 'input[type="password"]'],
10
+ submitButton: [
11
+ 'button[type="submit"]',
12
+ 'button.sign-in-form__submit-btn',
13
+ 'button:has-text("Sign in")',
14
+ ],
15
+ },
16
+
17
+ // 2FA
18
+ twoFA: {
19
+ pinInput: [
20
+ 'input#input__phone_verification_pin',
21
+ 'input[name="pin"]',
22
+ 'input[type="text"][*="pin" i]',
23
+ ],
24
+ submitButton: [
25
+ 'button[type="submit"]',
26
+ 'button:has-text("Submit")',
27
+ 'button:has-text("Verify")',
28
+ ],
29
+ },
30
+
31
+ // Navigation / logged in indicators
32
+ nav: {
33
+ globalNav: ['nav.global-nav', 'header.global-nav', '[data-testid="global-nav"]'],
34
+ profileDropdown: [
35
+ 'button[aria-label*="Settings"]',
36
+ '.global-nav__me-menu button',
37
+ '[data-testid="settings-menu-trigger"]',
38
+ ],
39
+ },
40
+
41
+ // Messaging
42
+ messages: {
43
+ conversationList: [
44
+ 'div.msg-conversations-container__conversations-list',
45
+ '[data-testid="conversations-list"]',
46
+ 'div[class*="conversations-list"]',
47
+ ],
48
+ conversationItem: [
49
+ 'div.msg-conversation-card',
50
+ '[data-testid="conversation-card"]',
51
+ 'div[class*="conversation-card"]',
52
+ ],
53
+ messageBubble: [
54
+ 'div.msg-s-message-group__message',
55
+ '[data-testid="message-bubble"]',
56
+ 'div[class*="message-bubble"]',
57
+ ],
58
+ messageInput: [
59
+ 'div.msg-form__contenteditable',
60
+ 'div[contenteditable="true"][role="textbox"]',
61
+ '[data-testid="message-input"]',
62
+ ],
63
+ sendButton: ['button.msg-form__send-btn', 'button[type="submit"]', 'button:has-text("Send")'],
64
+ },
65
+
66
+ // Connection requests
67
+ connection: {
68
+ // Primary connect button (various states)
69
+ // LinkedIn has many variants - we need comprehensive fallbacks
70
+ connectButton: [
71
+ // Aria-label variants
72
+ 'button[aria-label*="Connect"]',
73
+ 'button[aria-label*="Invite"]',
74
+ 'button[aria-label*="to connect"]',
75
+ // Text content variants
76
+ 'button:has-text("Connect")',
77
+ 'button:has-text("Invite")',
78
+ 'button:has-text("Connect ")',
79
+ // Class-based variants (LinkedIn uses artdeco classes)
80
+ 'button.artdeco-button--primary:has-text("Connect")',
81
+ 'button.artdeco-button--secondary:has-text("Connect")',
82
+ 'button.artdeco-button--primary',
83
+ 'button.artdeco-button--secondary',
84
+ // Specific profile page selectors
85
+ '.pv-top-card-v2-ctas button:has-text("Connect")',
86
+ '.profile-topcard-actions button:has-text("Connect")',
87
+ 'div.pv-top-card-v2-ctas button',
88
+ // Data test IDs
89
+ '[data-testid="connect-button"]',
90
+ '[data-testid="invite-button"]',
91
+ // Generic fallback - any button with connect-related text
92
+ 'button[id*="connect"]',
93
+ 'button[id*="invite"]',
94
+ ],
95
+ // More actions menu (three dots)
96
+ moreActionsButton: [
97
+ 'button[aria-label*="More actions"]',
98
+ 'button[aria-label*="More"]',
99
+ 'button:has-text("More")',
100
+ '.artdeco-dropdown__trigger',
101
+ 'button[data-testid="more-actions"]',
102
+ ],
103
+ // Connect option in dropdown menu
104
+ connectOptionInMenu: [
105
+ 'div[role="menuitem"]:has-text("Connect")',
106
+ 'button:has-text("Connect")',
107
+ '[role="menuitem"][aria-label*="Connect"]',
108
+ ],
109
+ // Add a note modal
110
+ addNoteButton: ['button[aria-label*="Add a note"]', 'button:has-text("Add a note")'],
111
+ noteTextarea: ['textarea[name="message"]', 'textarea[placeholder*="note"]', 'textarea'],
112
+ sendButton: [
113
+ // Aria-label variants
114
+ 'button[aria-label*="Send invitation"]',
115
+ 'button[aria-label*="Send invite"]',
116
+ 'button[aria-label="Send"]',
117
+ // Text content variants
118
+ 'button:has-text("Send")',
119
+ 'button:has-text("Send invitation")',
120
+ 'button:has-text("Send invite")',
121
+ // Modal-specific primary buttons
122
+ '.artdeco-modal button.artdeco-button--primary',
123
+ '.artdeco-modal button:has-text("Send")',
124
+ '.artdeco-modal button[type="submit"]',
125
+ // Generic modal submit
126
+ '[role="dialog"] button.artdeco-button--primary',
127
+ '[role="dialog"] button:has-text("Send")',
128
+ // Form submit in modal
129
+ 'form button[type="submit"]',
130
+ '.artdeco-modal form button.artdeco-button--primary',
131
+ // Generic fallback
132
+ 'button[type="submit"]',
133
+ '.artdeco-button--primary',
134
+ ],
135
+ },
136
+
137
+ // Company profile extraction
138
+ company: {
139
+ name: [
140
+ 'h1.text-heading-xlarge',
141
+ '.org-top-card-primary-content h1',
142
+ 'h1.org-top-card-summary__title',
143
+ ],
144
+ website: [
145
+ 'a[data-testid="website-link"]',
146
+ '.org-about-us-module__website a',
147
+ 'dd a[href^="http"]:not([href*="linkedin"])',
148
+ ],
149
+ industry: [
150
+ 'dt:has-text("Industry") + dd',
151
+ '.org-about-company-module__dl dt:has-text("Industry") + dd',
152
+ ],
153
+ company_size: [
154
+ 'dt:has-text("Company size") + dd',
155
+ 'dt:has-text("Employees") + dd',
156
+ '.org-about-company-module__dl dt:has-text("Company size") + dd',
157
+ ],
158
+ headquarters: [
159
+ 'dt:has-text("Headquarters") + dd',
160
+ '.org-about-company-module__dl dt:has-text("Headquarters") + dd',
161
+ ],
162
+ founded: [
163
+ 'dt:has-text("Founded") + dd',
164
+ '.org-about-company-module__dl dt:has-text("Founded") + dd',
165
+ ],
166
+ specialties: [
167
+ 'dt:has-text("Specialties") + dd',
168
+ '.org-about-company-module__dl dt:has-text("Specialties") + dd',
169
+ ],
170
+ type: [
171
+ 'dt:has-text("Company type") + dd',
172
+ 'dt:has-text("Type") + dd',
173
+ '.org-about-company-module__dl dt:has-text("Company type") + dd',
174
+ ],
175
+ follower_count: [
176
+ '.org-top-card-primary-content__followers-count',
177
+ '.org-top-card-module__followers-count',
178
+ 'span:has-text("followers")',
179
+ '.org-top-card-summary__followers',
180
+ ],
181
+ // Authwall dismiss button
182
+ dismissAuthwall: [
183
+ 'button[aria-label="Dismiss"]',
184
+ 'button[aria-label="Close"]',
185
+ '.artdeco-modal__dismiss',
186
+ 'button.artdeco-modal__dismiss',
187
+ ],
188
+ },
189
+
190
+ // Profile extraction
191
+ profile: {
192
+ // Top card
193
+ name: [
194
+ 'h1.text-heading-xlarge',
195
+ '[data-testid="top-card-profile-name"]',
196
+ '.pv-top-card .text-heading-xlarge',
197
+ 'section.artdeco-card h1',
198
+ ],
199
+ headline: [
200
+ '.text-body-medium',
201
+ '[data-testid="top-card-profile-headline"]',
202
+ '.pv-top-card .text-body-medium',
203
+ ],
204
+ location: [
205
+ '.text-body-small.inline.t-black--light',
206
+ '[data-testid="top-card-profile-location"]',
207
+ '.pv-top-card .text-body-small',
208
+ ],
209
+
210
+ // Experience section - find by heading text pattern
211
+ experienceSection: [
212
+ 'section:has(h2:has-text("Experience"))',
213
+ 'section[data-view-name="profile-card-experience"]',
214
+ 'section[id*="experience"]',
215
+ ],
216
+ experienceList: [
217
+ 'li[data-view-name="profile-component-entity"]',
218
+ 'section:has(h2:has-text("Experience")) li',
219
+ '.pvs-list__item',
220
+ ],
221
+ // Title: First .t-bold span in each experience item
222
+ experienceTitle: [
223
+ '.t-bold span[aria-hidden="true"]',
224
+ '.hoverable-link-text span[aria-hidden="true"]',
225
+ '.display-flex.align-items-center.t-bold span',
226
+ ],
227
+ // Company: First .t-14.t-normal span (after title)
228
+ experienceCompany: ['.t-14.t-normal span[aria-hidden="true"]', 'span[class*="t-14"]'],
229
+ // Duration: caption wrapper contains "X yrs Y mos"
230
+ experienceDuration: [
231
+ '.pvs-entity__caption-wrapper',
232
+ '.t-14.t-black--light span[aria-hidden="true"]',
233
+ 'span[class*="caption"]',
234
+ ],
235
+ companyLink: ['a[data-field="experience_company_logo"]', 'a[href*="/company/"]'],
236
+
237
+ // Contact info
238
+ contactInfoButton: [
239
+ 'a[href*="contact-info"]',
240
+ 'button[aria-label*="contact info" i]',
241
+ '[data-control-name="contact_see_more"]',
242
+ ],
243
+ contactInfoPanel: [
244
+ '.pv-contact-info',
245
+ '[data-testid="contact-info-panel"]',
246
+ '.artdeco-modal__content',
247
+ ],
248
+ contactInfoCloseButton: [
249
+ '.artdeco-modal__dismiss',
250
+ 'button[aria-label*="Dismiss"]',
251
+ 'button[aria-label*="Close"]',
252
+ '[data-testid="modal-close"]',
253
+ ],
254
+ email: [
255
+ 'a[href^="mailto:"]',
256
+ '[data-testid="contact-email"]',
257
+ '.pv-contact-info__contact-type[href*="mailto"]',
258
+ ],
259
+ phone: [
260
+ 'a[href^="tel:"]',
261
+ '[data-testid="contact-phone"]',
262
+ '.pv-contact-info__contact-type[href*="tel"]',
263
+ ],
264
+ } as const,
265
+ } as const;
266
+
267
+ export type SelectorCategory = keyof typeof SELECTORS;
268
+ export type SelectorKey<C extends SelectorCategory> = keyof (typeof SELECTORS)[C];
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: Follow-up Message
3
+ description: Follow-up after initial outreach
4
+ category: followup
5
+ ---
6
+
7
+ Hi {{firstName}},
8
+
9
+ Just following up on my previous message. I know you're busy, so no pressure at all.
10
+
11
+ I'd still love to connect when you have a moment. Let me know if you're interested!
12
+
13
+ Best,
14
+ {{senderName}}
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: Meeting Request
3
+ description: Request a meeting or call
4
+ category: meeting
5
+ ---
6
+
7
+ Hi {{firstName}},
8
+
9
+ I've been following {{company}}'s work and I'm really impressed with what you're building.
10
+
11
+ I have some ideas that might be relevant to your {{topic}} initiatives. Would you be open to a 15-minute call next week to explore this?
12
+
13
+ I'm flexible on timing - just let me know what works for you!
14
+
15
+ Best regards,
16
+ {{senderName}}