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,596 @@
1
+ # LinkedIn Connection Feature Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add a `connect` command to send LinkedIn connection requests to specific profiles, handling cases where the button is hidden behind the "..." menu.
6
+
7
+ **Architecture:**
8
+ - Add connection selectors to `selectors.ts` for various button states
9
+ - Create a `LinkedInConnector` class in `linkedin/connector.ts` to handle connection logic
10
+ - Add CLI command `linkedin connect <profile-url>` with option to add a note
11
+ - Handle the "More" (three dots) menu when the connect button is not directly visible
12
+
13
+ **Tech Stack:** TypeScript, Playwright, Commander.js
14
+
15
+ ---
16
+
17
+ ## Pre-Task: Complete Login First
18
+
19
+ **Status check:** Run `npm run build` and verify the project compiles.
20
+
21
+ **Command:**
22
+ ```bash
23
+ npm run build
24
+ ```
25
+
26
+ **Expected:** TypeScript compiles without errors.
27
+
28
+ ---
29
+
30
+ ## Task 1: Add Connection Selectors
31
+
32
+ **Files:**
33
+ - Modify: `src/linkedin/selectors.ts`
34
+
35
+ **Step 1: Add connection selectors to SELECTORS object**
36
+
37
+ Add a new `connection` section after the `messages` section:
38
+
39
+ ```typescript
40
+ // Connection requests
41
+ connection: {
42
+ // Primary connect button (various states)
43
+ connectButton: [
44
+ 'button[aria-label*="Connect"]',
45
+ 'button:has-text("Connect")',
46
+ 'button.connect',
47
+ '[data-testid="connect-button"]',
48
+ ],
49
+ // More actions menu (three dots)
50
+ moreActionsButton: [
51
+ 'button[aria-label*="More actions"]',
52
+ 'button[aria-label*="More"]',
53
+ 'button:has-text("More")',
54
+ '.artdeco-dropdown__trigger',
55
+ 'button[data-testid="more-actions"]',
56
+ ],
57
+ // Connect option in dropdown menu
58
+ connectOptionInMenu: [
59
+ 'div[role="menuitem"]:has-text("Connect")',
60
+ 'button:has-text("Connect")',
61
+ '[role="menuitem"][aria-label*="Connect"]',
62
+ ],
63
+ // Add a note modal
64
+ addNoteButton: [
65
+ 'button[aria-label*="Add a note"]',
66
+ 'button:has-text("Add a note")',
67
+ ],
68
+ noteTextarea: [
69
+ 'textarea[name="message"]',
70
+ 'textarea[placeholder*="note"]',
71
+ 'textarea',
72
+ ],
73
+ sendButton: [
74
+ 'button[aria-label*="Send invitation"]',
75
+ 'button:has-text("Send")',
76
+ 'button[type="submit"]',
77
+ ],
78
+ },
79
+ ```
80
+
81
+ **Step 2: Run build to verify no TypeScript errors**
82
+
83
+ ```bash
84
+ npm run build
85
+ ```
86
+
87
+ **Expected:** Compiles successfully.
88
+
89
+ **Step 3: Commit**
90
+
91
+ ```bash
92
+ git add src/linkedin/selectors.ts
93
+ git commit -m "feat: add connection selectors for LinkedIn connect feature"
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Task 2: Create LinkedInConnector Class
99
+
100
+ **Files:**
101
+ - Create: `src/linkedin/connector.ts`
102
+ - Create: `src/linkedin/__tests__/connector.test.ts` (optional, basic test)
103
+
104
+ **Step 1: Write the LinkedInConnector class**
105
+
106
+ ```typescript
107
+ import type { BrowserController } from '../core/browser';
108
+ import { SELECTORS } from './selectors';
109
+
110
+ export interface ConnectionResult {
111
+ success: boolean;
112
+ error?: string;
113
+ sent?: boolean;
114
+ pending?: boolean;
115
+ }
116
+
117
+ export interface ConnectionOptions {
118
+ profileUrl: string;
119
+ note?: string;
120
+ skipNote?: boolean;
121
+ }
122
+
123
+ export class LinkedInConnector {
124
+ private browser: BrowserController;
125
+
126
+ constructor(browser: BrowserController) {
127
+ this.browser = browser;
128
+ }
129
+
130
+ /**
131
+ * Send a connection request to a LinkedIn profile
132
+ */
133
+ async connect(options: ConnectionOptions): Promise<ConnectionResult> {
134
+ const page = this.browser.getPage();
135
+ if (!page) {
136
+ return { success: false, error: 'Browser not initialized' };
137
+ }
138
+
139
+ try {
140
+ // Navigate to the profile
141
+ console.log(`Navigating to ${options.profileUrl}...`);
142
+ await page.goto(options.profileUrl, {
143
+ waitUntil: 'domcontentloaded',
144
+ timeout: 30000,
145
+ });
146
+
147
+ // Wait a moment for any dynamic content
148
+ await page.waitForTimeout(2000);
149
+
150
+ // Try to find the Connect button
151
+ console.log('Looking for Connect button...');
152
+ const connectResult = await this.findConnectButton();
153
+
154
+ if (!connectResult) {
155
+ // Check if already connected or pending
156
+ const status = await this.checkConnectionStatus();
157
+ if (status === 'connected') {
158
+ return { success: true, sent: false, error: 'Already connected to this person' };
159
+ } else if (status === 'pending') {
160
+ return { success: true, pending: true, error: 'Connection request already sent (pending)' };
161
+ }
162
+ return { success: false, error: 'Could not find Connect button - LinkedIn UI may have changed' };
163
+ }
164
+
165
+ // Click the Connect button
166
+ console.log('Clicking Connect button...');
167
+ await connectResult.element.click();
168
+ await page.waitForTimeout(1000);
169
+
170
+ // Handle adding a note if specified
171
+ if (options.note && !options.skipNote) {
172
+ const noteResult = await this.findElementWithFallbacks(
173
+ SELECTORS.connection.addNoteButton,
174
+ 'Add a note button'
175
+ );
176
+
177
+ if (noteResult?.element) {
178
+ console.log('Clicking "Add a note" button...');
179
+ await noteResult.element.click();
180
+ await page.waitForTimeout(500);
181
+
182
+ // Find and fill the note textarea
183
+ const textareaResult = await this.findElementWithFallbacks(
184
+ SELECTORS.connection.noteTextarea,
185
+ 'Note textarea'
186
+ );
187
+
188
+ if (textareaResult?.element) {
189
+ console.log('Adding note...');
190
+ const page = this.browser.getPage();
191
+ if (page) {
192
+ await page.fill(textareaResult.selectorUsed, options.note);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ // Send the invitation
199
+ console.log('Sending connection request...');
200
+ const sendResult = await this.findElementWithFallbacks(
201
+ SELECTORS.connection.sendButton,
202
+ 'Send invitation button'
203
+ );
204
+
205
+ if (sendResult?.element) {
206
+ await sendResult.element.click();
207
+ await page.waitForTimeout(1000);
208
+ console.log('Connection request sent successfully!');
209
+ return { success: true, sent: true };
210
+ }
211
+
212
+ return { success: false, error: 'Could not find Send button' };
213
+
214
+ } catch (error) {
215
+ const errorMessage = error instanceof Error ? error.message : String(error);
216
+ console.error('Connection error:', errorMessage);
217
+ return { success: false, error: `Connection error: ${errorMessage}` };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Try to find the Connect button - either directly or in the More menu
223
+ */
224
+ private async findConnectButton(): Promise<{ element: Awaited<ReturnType<import('playwright').Page['$']>>; selectorUsed: string } | null> {
225
+ // First try direct Connect button
226
+ const directConnect = await this.findElementWithFallbacks(
227
+ SELECTORS.connection.connectButton,
228
+ 'direct Connect button'
229
+ );
230
+
231
+ if (directConnect) {
232
+ return directConnect;
233
+ }
234
+
235
+ console.log('Direct Connect button not found, checking More menu...');
236
+
237
+ // Try the More menu
238
+ const moreButton = await this.findElementWithFallbacks(
239
+ SELECTORS.connection.moreActionsButton,
240
+ 'More actions button'
241
+ );
242
+
243
+ if (moreButton?.element) {
244
+ console.log('Clicking More actions menu...');
245
+ const page = this.browser.getPage();
246
+ if (page) {
247
+ await moreButton.element.click();
248
+ await page.waitForTimeout(1000);
249
+
250
+ // Look for Connect option in the dropdown
251
+ const connectOption = await this.findElementWithFallbacks(
252
+ SELECTORS.connection.connectOptionInMenu,
253
+ 'Connect option in menu'
254
+ );
255
+
256
+ if (connectOption) {
257
+ return connectOption;
258
+ }
259
+ }
260
+ }
261
+
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * Check the current connection status with a person
267
+ */
268
+ private async checkConnectionStatus(): Promise<'connected' | 'pending' | 'none' | 'unknown'> {
269
+ const page = this.browser.getPage();
270
+ if (!page) return 'unknown';
271
+
272
+ try {
273
+ // Look for indicators of existing connection
274
+ const indicators = [
275
+ { selector: 'button[aria-label*="Message"] i', text: 'Message', status: 'connected' as const },
276
+ { selector: 'button:has-text("Pending")', text: 'Pending', status: 'pending' as const },
277
+ { selector: 'span:has-text("1st")', text: '1st', status: 'connected' as const },
278
+ ];
279
+
280
+ for (const indicator of indicators) {
281
+ const element = await page.$(indicator.selector);
282
+ if (element) {
283
+ return indicator.status;
284
+ }
285
+ }
286
+
287
+ return 'none';
288
+ } catch {
289
+ return 'unknown';
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Try to find an element using multiple selector strategies
295
+ */
296
+ private async findElementWithFallbacks(
297
+ selectors: readonly string[],
298
+ elementName: string
299
+ ): Promise<{ element: Awaited<ReturnType<import('playwright').Page['$']>>; selectorUsed: string } | null> {
300
+ const page = this.browser.getPage();
301
+ if (!page) return null;
302
+
303
+ for (const selector of selectors) {
304
+ try {
305
+ const element = await page.$(selector);
306
+ if (element) {
307
+ console.log(`Found ${elementName} using selector: ${selector}`);
308
+ return { element, selectorUsed: selector };
309
+ }
310
+ } catch {
311
+ // Continue to next selector
312
+ }
313
+ }
314
+
315
+ return null;
316
+ }
317
+ }
318
+ ```
319
+
320
+ **Step 2: Create the CLI command for connections**
321
+
322
+ Create `src/cli/connection.ts`:
323
+
324
+ ```typescript
325
+ import { Command } from 'commander';
326
+ import chalk from 'chalk';
327
+ import { BrowserController } from '../core/browser';
328
+ import { LinkedInConnector } from '../linkedin/connector';
329
+ import { getSecureStorage } from '../core/storage';
330
+ import { getAuditLogger } from '../core/audit';
331
+ import { getConfig } from '../core/config';
332
+
333
+ const SESSION_KEY = 'linkedin-session';
334
+
335
+ export function registerConnectionCommands(program: Command): void {
336
+ const connection = program
337
+ .command('connect')
338
+ .description('Send connection requests to LinkedIn profiles');
339
+
340
+ connection
341
+ .command('send')
342
+ .description('Send a connection request to a profile')
343
+ .argument('<profile-url>', 'LinkedIn profile URL (e.g., https://www.linkedin.com/in/williamhgates/)')
344
+ .option('-n, --note <note>', 'Personalized note to include with connection request (max 300 characters)')
345
+ .option('--headless', 'Run browser in headless mode', false)
346
+ .action(async (profileUrl, options) => {
347
+ const storage = getSecureStorage();
348
+ const logger = getAuditLogger();
349
+ const config = getConfig();
350
+
351
+ try {
352
+ // Validate profile URL
353
+ if (!profileUrl.match(/^https?:\/\/www\.linkedin\.com\/in\/[^\/]+\/?$/)) {
354
+ console.error(chalk.red('✗ Invalid profile URL'));
355
+ console.log(chalk.gray('Expected format: https://www.linkedin.com/in/username/'));
356
+ process.exit(1);
357
+ }
358
+
359
+ // Check for existing session
360
+ if (!storage.exists(SESSION_KEY)) {
361
+ console.error(chalk.red('✗ Not logged in'));
362
+ console.log(chalk.gray('Run: linkedin-cli auth login'));
363
+ process.exit(1);
364
+ }
365
+
366
+ const sessionData = storage.load(SESSION_KEY);
367
+ if (!sessionData) {
368
+ console.error(chalk.red('✗ Session data corrupted'));
369
+ console.log(chalk.gray('Run: linkedin-cli auth login'));
370
+ process.exit(1);
371
+ }
372
+
373
+ console.log(chalk.blue(`Preparing to connect with ${profileUrl}...`));
374
+
375
+ // Launch browser
376
+ const browser = new BrowserController({
377
+ headless: options.headless ?? config.getValue('headless'),
378
+ });
379
+
380
+ await browser.launch();
381
+
382
+ // Restore session
383
+ const session = JSON.parse(sessionData);
384
+ await browser.restoreSession(session);
385
+
386
+ // Create connector and send request
387
+ const connector = new LinkedInConnector(browser);
388
+ const result = await connector.connect({
389
+ profileUrl,
390
+ note: options.note,
391
+ });
392
+
393
+ if (result.success && result.sent) {
394
+ console.log(chalk.green('✓ Connection request sent successfully!'));
395
+ logger.log('connection.send', { profileUrl, hasNote: !!options.note }, true);
396
+ } else if (result.pending) {
397
+ console.log(chalk.yellow('⚠ Connection request already pending'));
398
+ } else if (result.error?.includes('Already connected')) {
399
+ console.log(chalk.yellow('✓ Already connected to this person'));
400
+ } else {
401
+ console.error(chalk.red('✗ Failed to send connection request:'), result.error);
402
+ logger.log('connection.send', { profileUrl, error: result.error }, false);
403
+ await browser.close();
404
+ process.exit(1);
405
+ }
406
+
407
+ await browser.close();
408
+ } catch (error) {
409
+ console.error(chalk.red('✗ Connection failed:'), error instanceof Error ? error.message : error);
410
+ process.exit(1);
411
+ }
412
+ });
413
+ }
414
+ ```
415
+
416
+ **Step 3: Register the command in the main CLI**
417
+
418
+ Modify `src/index.ts` to import and register the connection commands:
419
+
420
+ ```typescript
421
+ import { registerAuthCommands, registerMessageCommands, registerReplyCommands, registerConnectionCommands } from './cli';
422
+
423
+ // ... existing code ...
424
+
425
+ // Register command groups
426
+ registerAuthCommands(program);
427
+ registerMessageCommands(program);
428
+ registerReplyCommands(program);
429
+ registerConnectionCommands(program); // Add this line
430
+ ```
431
+
432
+ **Step 4: Export the command from cli/index.ts**
433
+
434
+ Modify `src/cli/index.ts` to export the connection commands:
435
+
436
+ ```typescript
437
+ export { registerAuthCommands } from './auth';
438
+ export { registerMessageCommands } from './messages';
439
+ export { registerReplyCommands } from './reply';
440
+ export { registerConnectionCommands } from './connection'; // Add this line
441
+ ```
442
+
443
+ **Step 5: Run build to verify no TypeScript errors**
444
+
445
+ ```bash
446
+ npm run build
447
+ ```
448
+
449
+ **Expected:** Compiles successfully.
450
+
451
+ **Step 6: Commit**
452
+
453
+ ```bash
454
+ git add src/linkedin/connector.ts src/cli/connection.ts src/cli/index.ts src/index.ts
455
+ git commit -m "feat: add connection command to send LinkedIn connection requests"
456
+ ```
457
+
458
+ ---
459
+
460
+ ## Task 3: Test Connection Feature
461
+
462
+ **Step 1: Verify the command is registered**
463
+
464
+ ```bash
465
+ node dist/index.js --help
466
+ ```
467
+
468
+ **Expected:** See `connect` command in the list.
469
+
470
+ **Step 2: Check help for connect command**
471
+
472
+ ```bash
473
+ node dist/index.js connect send --help
474
+ ```
475
+
476
+ **Expected:** Shows usage: `Usage: linkedin-cli connect send [options] <profile-url>`
477
+
478
+ **Step 3: Test without login (should fail with helpful message)**
479
+
480
+ ```bash
481
+ node dist/index.js connect send "https://www.linkedin.com/in/williamhgates/"
482
+ ```
483
+
484
+ **Expected:** Error message: "Not logged in. Run: linkedin-cli auth login"
485
+
486
+ **Step 4: Commit test results**
487
+
488
+ ```bash
489
+ git add -A
490
+ git commit -m "test: verify connection command structure"
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Task 4: Document the Connection Feature
496
+
497
+ **Files:**
498
+ - Create: `docs/connection-command.md`
499
+
500
+ **Step 1: Write documentation**
501
+
502
+ ```markdown
503
+ # Connection Command
504
+
505
+ Send LinkedIn connection requests via CLI.
506
+
507
+ ## Prerequisites
508
+
509
+ You must be logged in:
510
+ ```bash
511
+ linkedin-cli auth login
512
+ ```
513
+
514
+ ## Usage
515
+
516
+ ### Basic connection request
517
+ ```bash
518
+ linkedin-cli connect send "https://www.linkedin.com/in/williamhgates/"
519
+ ```
520
+
521
+ ### With a personalized note
522
+ ```bash
523
+ linkedin-cli connect send "https://www.linkedin.com/in/williamhgates/" \
524
+ --note "Hi Bill, I'd love to connect and discuss technology."
525
+ ```
526
+
527
+ ### Run in headless mode
528
+ ```bash
529
+ linkedin-cli connect send "https://www.linkedin.com/in/williamhgates/" --headless
530
+ ```
531
+
532
+ ## How It Works
533
+
534
+ 1. **Navigates to profile:** Opens the LinkedIn profile URL
535
+ 2. **Finds Connect button:** Tries multiple selectors to locate the Connect button
536
+ 3. **Handles "More" menu:** If Connect is hidden, clicks the "..." (More actions) button and selects Connect from the dropdown
537
+ 4. **Optional note:** If `--note` provided, clicks "Add a note", fills the textarea
538
+ 5. **Sends request:** Clicks "Send invitation"
539
+
540
+ ## Error Handling
541
+
542
+ - **Not logged in:** Prompts to run `linkedin-cli auth login`
543
+ - **Already connected:** Shows "Already connected to this person"
544
+ - **Pending request:** Shows "Connection request already pending"
545
+ - **Button not found:** Reports "LinkedIn UI may have changed" with debug info
546
+
547
+ ## Technical Details
548
+
549
+ The command uses a multi-layer selector system to handle LinkedIn's dynamic UI:
550
+ - Tries `aria-label` selectors first (accessibility-friendly)
551
+ - Falls back to text-based selectors (`:has-text()`)
552
+ - Uses class-based selectors as final fallback
553
+
554
+ This approach makes the tool resilient to LinkedIn's frequent UI updates.
555
+ ```
556
+
557
+ **Step 2: Commit documentation**
558
+
559
+ ```bash
560
+ git add docs/connection-command.md
561
+ git commit -m "docs: add connection command documentation"
562
+ ```
563
+
564
+ ---
565
+
566
+ ## Final Verification
567
+
568
+ **Run all tests and verify build:**
569
+
570
+ ```bash
571
+ npm run build
572
+ node dist/index.js --help
573
+ node dist/index.js connect send --help
574
+ ```
575
+
576
+ **Final commit:**
577
+
578
+ ```bash
579
+ git add -A
580
+ git commit -m "feat: complete LinkedIn connection request feature"
581
+ ```
582
+
583
+ ---
584
+
585
+ ## Execution Summary
586
+
587
+ This implementation adds a complete `connect` command that:
588
+
589
+ 1. **Accepts profile URLs** - Validates LinkedIn profile URL format
590
+ 2. **Supports personalized notes** - `--note` option for custom messages
591
+ 3. **Handles hidden buttons** - Clicks "..." menu when Connect is not directly visible
592
+ 4. **Provides clear feedback** - Success/error messages for all scenarios
593
+ 5. **Uses resilient selectors** - Multi-layer fallback system
594
+ 6. **Includes full documentation** - Usage guide and technical details
595
+
596
+ **Estimated implementation time:** 45-60 minutes for all tasks.