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,480 @@
1
+ # Messages Send 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 new `messages send` command that can start a new conversation with a LinkedIn profile URL.
6
+
7
+ **Architecture:** Create a new `LinkedInMessageSender` class that navigates to a profile page, finds and clicks the "Message" button, and sends a message. The CLI command will accept a profile URL and message text.
8
+
9
+ **Tech Stack:** TypeScript, Playwright for browser automation, Commander.js for CLI
10
+
11
+ ---
12
+
13
+ ### Task 1: Add TypeScript types for send message feature
14
+
15
+ **Files:**
16
+ - Modify: `src/types/index.ts:17-29`
17
+
18
+ **Step 1: Add new types to types/index.ts**
19
+
20
+ Add after the `Message` interface (around line 29):
21
+
22
+ ```typescript
23
+ // New message sending types
24
+ export interface SendMessageOptions {
25
+ profileUrl: string;
26
+ text: string;
27
+ dryRun?: boolean;
28
+ }
29
+
30
+ export interface SendMessageResult {
31
+ success: boolean;
32
+ messageId?: string;
33
+ threadId?: string;
34
+ error?: string;
35
+ }
36
+ ```
37
+
38
+ **Step 2: Verify types compile**
39
+
40
+ Run: `npm run build`
41
+ Expected: No errors
42
+
43
+ **Step 3: Commit**
44
+
45
+ ```bash
46
+ git add src/types/index.ts
47
+ git commit -m "types: add SendMessageOptions and SendMessageResult interfaces"
48
+ ```
49
+
50
+ ---
51
+
52
+ ### Task 2: Create LinkedInMessageSender class
53
+
54
+ **Files:**
55
+ - Create: `src/linkedin/message-sender.ts`
56
+
57
+ **Step 1: Create the message sender class**
58
+
59
+ ```typescript
60
+ import type { Page } from 'playwright';
61
+ import { SelectorEngine } from './selector-engine';
62
+
63
+ export interface SendMessageOptions {
64
+ profileUrl: string;
65
+ text: string;
66
+ dryRun?: boolean;
67
+ }
68
+
69
+ export interface SendMessageResult {
70
+ success: boolean;
71
+ messageId?: string;
72
+ threadId?: string;
73
+ error?: string;
74
+ }
75
+
76
+ export class LinkedInMessageSender {
77
+ private page: Page;
78
+ private selectorEngine: SelectorEngine;
79
+
80
+ constructor(page: Page) {
81
+ this.page = page;
82
+ this.selectorEngine = new SelectorEngine(page);
83
+ }
84
+
85
+ /**
86
+ * Send a message to a LinkedIn profile
87
+ */
88
+ async sendMessage(options: SendMessageOptions): Promise<SendMessageResult> {
89
+ try {
90
+ // Navigate to profile
91
+ await this.page.goto(options.profileUrl, {
92
+ waitUntil: 'networkidle',
93
+ timeout: 30000,
94
+ });
95
+
96
+ // Wait for page to load
97
+ await this.page.waitForTimeout(2000);
98
+
99
+ // Find and click the Message button
100
+ const messageButton = await this.findMessageButton();
101
+ if (!messageButton) {
102
+ return { success: false, error: 'Could not find Message button on profile' };
103
+ }
104
+
105
+ // Click message button - this opens or navigates to conversation
106
+ await messageButton.click();
107
+
108
+ // Wait for messaging UI to load
109
+ await this.page.waitForTimeout(2000);
110
+
111
+ // Wait for message input field
112
+ const inputResult = await this.selectorEngine.findElement('messages', 'messageInput', {
113
+ timeout: 10000,
114
+ visible: true,
115
+ });
116
+
117
+ if (!inputResult.element) {
118
+ return { success: false, error: 'Could not find message input field' };
119
+ }
120
+
121
+ // Type the message
122
+ await inputResult.element.fill(options.text);
123
+
124
+ // Check if dry run
125
+ if (options.dryRun) {
126
+ return {
127
+ success: true,
128
+ messageId: `dry-run-${Date.now()}`,
129
+ threadId: this.extractThreadId(),
130
+ };
131
+ }
132
+
133
+ // Click send button
134
+ const sendResult = await this.selectorEngine.findElement('messages', 'sendButton', {
135
+ timeout: 5000,
136
+ visible: true,
137
+ });
138
+
139
+ if (!sendResult.element) {
140
+ return { success: false, error: 'Could not find send button' };
141
+ }
142
+
143
+ await sendResult.element.click();
144
+
145
+ // Wait for message to be sent
146
+ await this.page.waitForTimeout(1000);
147
+
148
+ // Try to get message ID
149
+ const messageId = await this.page.evaluate(() => {
150
+ const lastMessage = document.querySelector('[data-urn*="urn:li:fsd_message:"]');
151
+ if (lastMessage) {
152
+ const urn = lastMessage.getAttribute('data-urn');
153
+ if (urn) {
154
+ const match = urn.match(/urn:li:fsd_message:(\d+)/);
155
+ return match ? match[1] : urn;
156
+ }
157
+ }
158
+ return `msg-${Date.now()}`;
159
+ });
160
+
161
+ return {
162
+ success: true,
163
+ messageId,
164
+ threadId: this.extractThreadId(),
165
+ };
166
+ } catch (error) {
167
+ return {
168
+ success: false,
169
+ error: error instanceof Error ? error.message : 'Unknown error sending message',
170
+ };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Find the Message button on a profile page
176
+ */
177
+ private async findMessageButton() {
178
+ // Try multiple selector strategies
179
+ const selectors = [
180
+ 'button[aria-label*="Message"]',
181
+ 'button:has-text("Message")',
182
+ 'a[href*="messaging/compose"]',
183
+ '[data-test-message-btn]',
184
+ ];
185
+
186
+ for (const selector of selectors) {
187
+ try {
188
+ const button = await this.page.$(selector);
189
+ if (button) {
190
+ return button;
191
+ }
192
+ } catch {
193
+ // Continue to next selector
194
+ }
195
+ }
196
+
197
+ return null;
198
+ }
199
+
200
+ /**
201
+ * Extract thread ID from current URL
202
+ */
203
+ private extractThreadId(): string | undefined {
204
+ const url = this.page.url();
205
+ const match = url.match(/\/messaging\/thread\/([^/]+)/);
206
+ return match ? match[1] : undefined;
207
+ }
208
+ }
209
+ ```
210
+
211
+ **Step 2: Verify types compile**
212
+
213
+ Run: `npm run build`
214
+ Expected: No errors
215
+
216
+ **Step 3: Commit**
217
+
218
+ ```bash
219
+ git add src/linkedin/message-sender.ts
220
+ git commit -m "feat(message-sender): create LinkedInMessageSender class for new conversations"
221
+ ```
222
+
223
+ ---
224
+
225
+ ### Task 3: Add selectors for message sending
226
+
227
+ **Files:**
228
+ - Modify: `src/linkedin/selectors.ts:42-64`
229
+
230
+ **Step 1: Add message button selector**
231
+
232
+ Add to the `messages` section:
233
+
234
+ ```typescript
235
+ messages: {
236
+ // ... existing selectors ...
237
+ messageButton: [
238
+ 'button[aria-label*="Message"]',
239
+ 'button:has-text("Message")',
240
+ 'a[href*="messaging/compose"]',
241
+ '[data-test-message-btn]',
242
+ ],
243
+ // ... rest of existing selectors ...
244
+ },
245
+ ```
246
+
247
+ **Step 2: Verify compile**
248
+
249
+ Run: `npm run build`
250
+ Expected: Success
251
+
252
+ **Step 3: Commit**
253
+
254
+ ```bash
255
+ git add src/linkedin/selectors.ts
256
+ git commit -m "selectors: add message button selectors"
257
+ ```
258
+
259
+ ---
260
+
261
+ ### Task 4: Create CLI command for messages send
262
+
263
+ **Files:**
264
+ - Modify: `src/cli/messages.ts`
265
+
266
+ **Step 1: Add send command to registerMessageCommands**
267
+
268
+ Add after the `show` command (around line 187):
269
+
270
+ ```typescript
271
+ // Send new message
272
+ messages
273
+ .command('send')
274
+ .description('Send a new message to a LinkedIn profile')
275
+ .requiredOption('-u, --url <profile>', 'LinkedIn profile URL')
276
+ .requiredOption('-m, --message <text>', 'Message text to send')
277
+ .option('--dry-run', 'Simulate sending without actually sending', false)
278
+ .option('--headless', 'Run browser in headless mode', false)
279
+ .action(async (options) => {
280
+ const spinner = ora('Preparing to send message...').start();
281
+
282
+ try {
283
+ spinner.text = 'Launching browser...';
284
+
285
+ // Launch browser - will connect to existing Edge/Chrome via CDP if available
286
+ const config = getConfig();
287
+ const browser = new BrowserController({
288
+ headless: options.headless ?? config.getValue('headless'),
289
+ });
290
+
291
+ await browser.launch();
292
+
293
+ // Get the page
294
+ const page = browser.getPage();
295
+ if (!page) throw new Error('Browser page not available');
296
+
297
+ spinner.text = options.dryRun ? 'Simulating message send...' : 'Sending message...';
298
+
299
+ const sender = new LinkedInMessageSender(page);
300
+
301
+ const result = await sender.sendMessage({
302
+ profileUrl: options.url,
303
+ text: options.message,
304
+ dryRun: options.dryRun,
305
+ });
306
+
307
+ if (!result.success) {
308
+ spinner.fail(`Failed to send message: ${result.error}`);
309
+ await browser.close();
310
+ process.exit(1);
311
+ }
312
+
313
+ if (options.dryRun) {
314
+ spinner.succeed('Dry run completed - message would have been sent');
315
+ console.log(chalk.gray('Profile URL: ' + options.url));
316
+ console.log(chalk.gray('Message content:'));
317
+ console.log(chalk.cyan(options.message));
318
+ if (result.threadId) {
319
+ console.log(chalk.gray(`Thread ID: ${result.threadId}`));
320
+ }
321
+ } else {
322
+ spinner.succeed('Message sent successfully!');
323
+ console.log(chalk.gray(`Thread ID: ${result.threadId}`));
324
+ if (result.messageId) {
325
+ console.log(chalk.gray(`Message ID: ${result.messageId}`));
326
+ }
327
+ }
328
+
329
+ // Log action
330
+ const logger = getAuditLogger();
331
+ logger.log(
332
+ options.dryRun ? 'messages.send.dry-run' : 'messages.send',
333
+ { profileUrl: options.url, messageLength: options.message.length },
334
+ true
335
+ );
336
+
337
+ await browser.close();
338
+ } catch (error) {
339
+ spinner.fail('Failed to send message');
340
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
341
+ process.exit(1);
342
+ }
343
+ });
344
+ ```
345
+
346
+ **Step 2: Add import for LinkedInMessageSender**
347
+
348
+ At the top of the file, add:
349
+
350
+ ```typescript
351
+ import { LinkedInMessageSender } from '../linkedin/message-sender';
352
+ ```
353
+
354
+ **Step 3: Verify compile**
355
+
356
+ Run: `npm run build`
357
+ Expected: Success
358
+
359
+ **Step 4: Commit**
360
+
361
+ ```bash
362
+ git add src/cli/messages.ts
363
+ git commit -m "cli: add messages send command for new conversations"
364
+ ```
365
+
366
+ ---
367
+
368
+ ### Task 5: Test with Lily's profile
369
+
370
+ **Files:**
371
+ - No file changes
372
+
373
+ **Step 1: Build the project**
374
+
375
+ Run: `npm run build`
376
+ Expected: Success
377
+
378
+ **Step 2: Test dry run first**
379
+
380
+ Run:
381
+ ```bash
382
+ node dist/index.js messages send -u "https://www.linkedin.com/in/lily-q-7145971b9/" -m "Hi Lily, this is a test message from the LinkedIn CLI automation tool. Please ignore." --dry-run
383
+ ```
384
+
385
+ Expected output:
386
+ ```
387
+ ✔ Dry run completed - message would have been sent
388
+ Profile URL: https://www.linkedin.com/in/lily-q-7145971b9/
389
+ Message content:
390
+ Hi Lily, this is a test message from the LinkedIn CLI automation tool. Please ignore.
391
+ ```
392
+
393
+ **Step 3: Test actual message send**
394
+
395
+ Run:
396
+ ```bash
397
+ node dist/index.js messages send -u "https://www.linkedin.com/in/lily-q-7145971b9/" -m "Hi Lily, this is Thaddeus's LinkedIn automation CLI testing the new message sending feature. This is just a test - no action needed on your end. Thanks!"
398
+ ```
399
+
400
+ Expected output:
401
+ ```
402
+ ✔ Message sent successfully!
403
+ Thread ID: 2-xxxxx...
404
+ Message ID: xxxxx
405
+ ```
406
+
407
+ **Step 4: Verify message in LinkedIn**
408
+
409
+ Open your browser and check the messaging conversation with Lily to confirm the message was sent.
410
+
411
+ **Step 5: Commit any fixes if needed**
412
+
413
+ If any issues are found during testing, fix them and commit:
414
+
415
+ ```bash
416
+ git add src/linkedin/message-sender.ts
417
+ git commit -m "fix: message sender adjustments based on testing"
418
+ ```
419
+
420
+ ---
421
+
422
+ ### Task 6: Add help documentation
423
+
424
+ **Files:**
425
+ - Modify: `README.md` (if exists in project)
426
+
427
+ **Step 1: Add documentation for the new command**
428
+
429
+ Add to the Messages section:
430
+
431
+ ```markdown
432
+ ### Send a new message
433
+
434
+ Send a message to a LinkedIn profile (starts a new conversation):
435
+
436
+ ```bash
437
+ linkedin-cli messages send -u "https://www.linkedin.com/in/profile-url/" -m "Hello!"
438
+
439
+ # Dry run first (recommended)
440
+ linkedin-cli messages send -u "https://www.linkedin.com/in/profile-url/" -m "Hello!" --dry-run
441
+ ```
442
+
443
+ Options:
444
+ - `-u, --url <profile>` - LinkedIn profile URL (required)
445
+ - `-m, --message <text>` - Message text to send (required)
446
+ - `--dry-run` - Simulate without sending
447
+ - `--headless` - Run browser in headless mode
448
+ ```
449
+
450
+ **Step 2: Commit**
451
+
452
+ ```bash
453
+ git add README.md
454
+ git commit -m "docs: add messages send command documentation"
455
+ ```
456
+
457
+ ---
458
+
459
+ ## Testing Checklist
460
+
461
+ - [ ] Dry run works without errors
462
+ - [ ] Actual message sends successfully
463
+ - [ ] Error handling works (invalid URL, no message button, etc.)
464
+ - [ ] CDP integration works (uses existing browser session)
465
+ - [ ] Thread ID is correctly extracted
466
+ - [ ] Audit logging works
467
+ - [ ] Help command shows new options
468
+
469
+ ## Estimated Effort
470
+
471
+ - **Implementation:** 2-3 hours
472
+ - **Testing:** 30 minutes
473
+ - **Total:** ~3 hours
474
+
475
+ ---
476
+
477
+ ## Next Steps
478
+
479
+ 1. ✅ Plan complete and saved to `docs/plans/2026-02-28-messages-send-feature.md`
480
+ 2. ⏳ Execute plan with `superpowers:executing-plans`
@@ -0,0 +1,243 @@
1
+ # v0.3.0 Design: Fix `messages show` Command
2
+
3
+ ## Overview
4
+
5
+ Fix the `messages show` command to display full conversation threads by implementing click-to-navigate URL capture for extracting real LinkedIn thread IDs.
6
+
7
+ **v0.2.0 Problem:** Thread IDs are fallback ember IDs (ember50, ember58) which don't work for navigation.
8
+
9
+ **v0.3.0 Solution:** Click conversation cards → capture URL → extract real thread ID → scrape messages.
10
+
11
+ ---
12
+
13
+ ## Requirements
14
+
15
+ ### Functional Requirements
16
+
17
+ 1. **Show Thread by ID**
18
+ - `linkedin-cli messages show -t ember50` displays full conversation
19
+ - Works with both ember IDs and real thread IDs
20
+
21
+ 2. **Message Display**
22
+ - Default: Human-readable Style A (chronological chat format)
23
+ - `--json` flag: Structured JSON for LLM/automation processing
24
+
25
+ 3. **Message Metadata**
26
+ - Sender name (with "You" indicator for current user)
27
+ - Timestamp (formatted + raw in JSON)
28
+ - Message content (with newline preservation)
29
+
30
+ ### Non-Functional Requirements
31
+
32
+ - **Reliability:** Handle LinkedIn's dynamic UI with fallback strategies
33
+ - **Performance:** Load messages within 10 seconds for typical conversations
34
+ - **Rate Limiting:** Respect LinkedIn's rate limits (max 1 request/second)
35
+
36
+ ---
37
+
38
+ ## Architecture
39
+
40
+ ### Component Diagram
41
+
42
+ ```
43
+ ┌─────────────────────────────────────────────────────────────────┐
44
+ │ messages show Command │
45
+ ├─────────────────────────────────────────────────────────────────┤
46
+ │ │
47
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
48
+ │ │ Card Locator │───▶│ Click & Wait │───▶│ URL Extractor│ │
49
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
50
+ │ │ │
51
+ │ ▼ │
52
+ │ ┌──────────────┐ │
53
+ │ │ Message │ │
54
+ │ │ Scraper │ │
55
+ │ └──────────────┘ │
56
+ │ │ │
57
+ │ ┌──────────────┐ ▼ │
58
+ │ │ JSON Output │◀───┐ ┌──────────────┐ │
59
+ │ │ Formatter │ └─│ Text Output │ │
60
+ │ └──────────────┘ └──────────────┘ │
61
+ └─────────────────────────────────────────────────────────────────┘
62
+ ```
63
+
64
+ ### Data Flow
65
+
66
+ 1. **User Input:** `messages show -t ember50 [--json]`
67
+ 2. **Card Lookup:** Find conversation card by ember ID in DOM
68
+ 3. **Navigation:** Click card → wait for URL change
69
+ 4. **ID Extraction:** Parse real thread ID from new URL
70
+ 5. **Message Scraping:** Extract all message bubbles
71
+ 6. **Output:** Format as text (default) or JSON
72
+
73
+ ---
74
+
75
+ ## Implementation Details
76
+
77
+ ### 1. Card Locator (`findConversationCard()`)
78
+
79
+ ```typescript
80
+ async findConversationCard(cardId: string): Promise<ElementHandle> {
81
+ // Try direct ember ID match
82
+ let card = await page.$(`#conversation-card-${cardId}`);
83
+
84
+ // Fallback: search by class + index
85
+ if (!card) {
86
+ const cards = await page.$$('.msg-conversation-card');
87
+ const index = cardId.replace('ember', '');
88
+ card = cards[parseInt(index)];
89
+ }
90
+
91
+ return card;
92
+ }
93
+ ```
94
+
95
+ ### 2. Click & Navigate
96
+
97
+ ```typescript
98
+ async navigateToThread(card: ElementHandle): Promise<string> {
99
+ // Set up URL change listener
100
+ const [newUrl] = await Promise.all([
101
+ page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
102
+ card.click(),
103
+ ]);
104
+
105
+ return page.url(); // Contains real thread ID
106
+ }
107
+ ```
108
+
109
+ ### 3. Message Scraper
110
+
111
+ ```typescript
112
+ async scrapeMessages(threadId: string): Promise<Message[]> {
113
+ const messageBubbles = await page.$$('.msg-s-message-group__message');
114
+
115
+ return Promise.all(messageBubbles.map(async (bubble) => {
116
+ return {
117
+ sender: await bubble.$eval('.msg-s-message-group__name', el => el.textContent),
118
+ content: await bubble.$eval('.msg-s-event-listitem__body', el => el.textContent),
119
+ timestamp: await bubble.$eval('time', el => el.getAttribute('datetime')),
120
+ isMe: await bubble.evaluate(el => el.classList.contains('msg-s-message-group--me')),
121
+ };
122
+ }));
123
+ }
124
+ ```
125
+
126
+ ### 4. Output Formatters
127
+
128
+ **Text Formatter (Default):**
129
+ ```
130
+ Conversations with Lucia Söffge
131
+ ────────────────────────────────
132
+ Feb 26, 2024
133
+
134
+ 10:30 AM Lucia: Hey! How are you?
135
+ 10:32 AM You: I'm good, thanks!
136
+ ```
137
+
138
+ **JSON Formatter (`--json`):**
139
+ ```json
140
+ {
141
+ "threadId": "2-ZWY1Mzkx...",
142
+ "participants": [{"name": "Lucia Söffge", "profileUrl": null}],
143
+ "messages": [
144
+ {
145
+ "id": "msg-1",
146
+ "threadId": "2-ZWY1Mzkx...",
147
+ "sender": {"name": "Lucia Söffge", "isMe": false},
148
+ "content": "Hey! How are you?",
149
+ "timestamp": "2024-02-26T10:30:00Z"
150
+ }
151
+ ]
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Error Handling
158
+
159
+ | Error | Handling |
160
+ |-------|----------|
161
+ | Card not found | Show list of available ember IDs |
162
+ | Navigation timeout | Retry click, then fail with message |
163
+ | No messages found | Show empty state with thread info |
164
+ | Rate limited | Wait + retry with exponential backoff |
165
+ | Session expired | Prompt user to re-authenticate |
166
+
167
+ ---
168
+
169
+ ## Testing Strategy
170
+
171
+ ### Unit Tests
172
+ - [ ] `findConversationCard()` with valid/invalid IDs
173
+ - [ ] URL extraction regex for thread IDs
174
+ - [ ] Message scraper with various message types
175
+
176
+ ### Integration Tests
177
+ - [ ] Click navigation with real LinkedIn session
178
+ - [ ] JSON output validation against schema
179
+ - [ ] Text output formatting
180
+
181
+ ### Manual Testing
182
+ - [ ] Conversations with 1 message
183
+ - [ ] Conversations with 100+ messages
184
+ - [ ] Conversations with attachments
185
+ - [ ] Group conversations (3+ participants)
186
+
187
+ ---
188
+
189
+ ## Success Criteria
190
+
191
+ - [ ] `messages show -t ember50` displays full thread
192
+ - [ ] `messages show -t ember50 --json` outputs valid JSON
193
+ - [ ] Works with real thread IDs (e.g., `2-ZWY1...`)
194
+ - [ ] Handles conversations up to 100 messages
195
+ - [ ] All tests pass (unit + integration)
196
+
197
+ ---
198
+
199
+ ## Out of Scope (v0.4.0+)
200
+
201
+ - [ ] Message attachments/images
202
+ - [ ] Message reactions
203
+ - [ ] Edit/delete messages
204
+ - [ ] Search within conversations
205
+ - [ ] Export conversations to file
206
+
207
+ ---
208
+
209
+ ## Files to Modify
210
+
211
+ | File | Changes |
212
+ |------|---------|
213
+ | `src/linkedin/messages.ts` | Add `showThread()` method with click navigation |
214
+ | `src/cli/messages.ts` | Add `--json` flag, update output formatting |
215
+ | `src/types/index.ts` | Add/verify `Message` and `Thread` types |
216
+ | `src/linkedin/selector-engine.ts` | Add message-specific selectors |
217
+
218
+ ---
219
+
220
+ ## Estimated Effort
221
+
222
+ - **Implementation:** 3-4 hours
223
+ - **Testing:** 1-2 hours
224
+ - **Documentation:** 30 minutes
225
+ - **Total:** 5-6 hours
226
+
227
+ ---
228
+
229
+ ## Approval
230
+
231
+ **Approved by:** [User]
232
+ **Date:** 2026-02-28
233
+ **Status:** Ready for implementation planning
234
+
235
+ ---
236
+
237
+ ## Next Steps
238
+
239
+ 1. ✅ Design approved
240
+ 2. ⏳ Invoke `writing-plans` skill for detailed implementation plan
241
+ 3. ⏳ Implement features
242
+ 4. ⏳ Test with real LinkedIn data
243
+ 5. ⏳ Release v0.3.0