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,115 @@
1
+ package ratelimit
2
+
3
+ import (
4
+ "fmt"
5
+ "math/rand"
6
+ "time"
7
+
8
+ "github.com/thaddeus-git/linkedin-cli/internal/config"
9
+ )
10
+
11
+ // Limiter handles rate limiting logic
12
+ type Limiter struct {
13
+ limits Limits
14
+ config *config.Manager
15
+ profile string
16
+ }
17
+
18
+ // NewLimiter creates a new rate limiter
19
+ func NewLimiter(profile string, limits Limits, cfg *config.Manager) *Limiter {
20
+ return &Limiter{
21
+ limits: limits,
22
+ config: cfg,
23
+ profile: profile,
24
+ }
25
+ }
26
+
27
+ // CheckConnection checks if a connection request is allowed
28
+ func (l *Limiter) CheckConnection() error {
29
+ state, err := l.config.LoadRateLimits(l.profile)
30
+ if err != nil {
31
+ return fmt.Errorf("failed to load rate limits: %w", err)
32
+ }
33
+
34
+ if !state.CanConnect(l.limits.ConnectionsDaily, l.limits.ConnectionsWeekly) {
35
+ return fmt.Errorf(
36
+ "rate limit exceeded: %d/%d daily, %d/%d weekly connections",
37
+ state.Connections.Today,
38
+ l.limits.ConnectionsDaily,
39
+ state.Connections.ThisWeek,
40
+ l.limits.ConnectionsWeekly,
41
+ )
42
+ }
43
+
44
+ return nil
45
+ }
46
+
47
+ // CheckMessage checks if a message is allowed
48
+ func (l *Limiter) CheckMessage() error {
49
+ state, err := l.config.LoadRateLimits(l.profile)
50
+ if err != nil {
51
+ return fmt.Errorf("failed to load rate limits: %w", err)
52
+ }
53
+
54
+ if !state.CanMessage(l.limits.MessagesDaily) {
55
+ return fmt.Errorf(
56
+ "rate limit exceeded: %d/%d daily messages",
57
+ state.Messages.Today,
58
+ l.limits.MessagesDaily,
59
+ )
60
+ }
61
+
62
+ return nil
63
+ }
64
+
65
+ // RecordConnection records a connection request
66
+ func (l *Limiter) RecordConnection() error {
67
+ state, err := l.config.LoadRateLimits(l.profile)
68
+ if err != nil {
69
+ return err
70
+ }
71
+
72
+ state.RecordConnection()
73
+ return l.config.SaveRateLimits(l.profile, state)
74
+ }
75
+
76
+ // RecordMessage records a message
77
+ func (l *Limiter) RecordMessage() error {
78
+ state, err := l.config.LoadRateLimits(l.profile)
79
+ if err != nil {
80
+ return err
81
+ }
82
+
83
+ state.RecordMessage()
84
+ return l.config.SaveRateLimits(l.profile, state)
85
+ }
86
+
87
+ // GetDelay returns a random delay between min and max seconds
88
+ func (l *Limiter) GetDelay() time.Duration {
89
+ seconds := rand.Intn(l.limits.MaxDelaySeconds-l.limits.MinDelaySeconds+1) + l.limits.MinDelaySeconds
90
+ return time.Duration(seconds) * time.Second
91
+ }
92
+
93
+ // Sleep delays execution for a random duration
94
+ func (l *Limiter) Sleep() {
95
+ time.Sleep(l.GetDelay())
96
+ }
97
+
98
+ // Status returns current rate limit status
99
+ func (l *Limiter) Status() (map[string]interface{}, error) {
100
+ state, err := l.config.LoadRateLimits(l.profile)
101
+ if err != nil {
102
+ return nil, err
103
+ }
104
+
105
+ return map[string]interface{}{
106
+ "connections_today": state.Connections.Today,
107
+ "connections_weekly": state.Connections.ThisWeek,
108
+ "connections_limit": l.limits.ConnectionsDaily,
109
+ "connections_weekly_limit": l.limits.ConnectionsWeekly,
110
+ "messages_today": state.Messages.Today,
111
+ "messages_limit": l.limits.MessagesDaily,
112
+ "last_connection": state.Connections.LastAction,
113
+ "last_message": state.Messages.LastAction,
114
+ }, nil
115
+ }
@@ -0,0 +1,32 @@
1
+ package ratelimit
2
+
3
+ // Limits defines rate limit thresholds
4
+ type Limits struct {
5
+ ConnectionsDaily int
6
+ ConnectionsWeekly int
7
+ MessagesDaily int
8
+ MinDelaySeconds int
9
+ MaxDelaySeconds int
10
+ }
11
+
12
+ // DefaultLimits returns default LinkedIn-safe limits
13
+ func DefaultLimits() Limits {
14
+ return Limits{
15
+ ConnectionsDaily: 20,
16
+ ConnectionsWeekly: 100,
17
+ MessagesDaily: 50,
18
+ MinDelaySeconds: 3,
19
+ MaxDelaySeconds: 8,
20
+ }
21
+ }
22
+
23
+ // ConservativeLimits returns more conservative limits for new accounts
24
+ func ConservativeLimits() Limits {
25
+ return Limits{
26
+ ConnectionsDaily: 10,
27
+ ConnectionsWeekly: 50,
28
+ MessagesDaily: 25,
29
+ MinDelaySeconds: 5,
30
+ MaxDelaySeconds: 12,
31
+ }
32
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "linkedin-automation-cli",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool for LinkedIn automation using Playwright",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "linkedin-automation": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "ts-node src/index.ts",
13
+ "watch": "nodemon --exec ts-node src/index.ts",
14
+ "clean": "rm -rf dist",
15
+ "typecheck": "tsc --noEmit",
16
+ "lint": "eslint src",
17
+ "lint:fix": "eslint src --fix",
18
+ "format": "prettier --write \"src/**/*.ts\"",
19
+ "format:check": "prettier --check \"src/**/*.ts\"",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage",
23
+ "prepare": "husky"
24
+ },
25
+ "keywords": [
26
+ "linkedin",
27
+ "automation",
28
+ "playwright",
29
+ "cli"
30
+ ],
31
+ "author": "",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@types/ws": "^8.18.1",
35
+ "chalk": "^4.1.2",
36
+ "commander": "^11.1.0",
37
+ "dotenv": "^16.3.1",
38
+ "inquirer": "^9.2.12",
39
+ "ora": "^5.4.1",
40
+ "playwright": "^1.40.0",
41
+ "sql.js": "^1.14.0",
42
+ "ws": "^8.19.0",
43
+ "zod": "^3.22.4"
44
+ },
45
+ "devDependencies": {
46
+ "@types/inquirer": "^9.0.7",
47
+ "@types/node": "^20.10.0",
48
+ "@typescript-eslint/eslint-plugin": "^6.13.0",
49
+ "@typescript-eslint/parser": "^6.13.0",
50
+ "@vitest/coverage-v8": "^1.0.4",
51
+ "eslint": "^8.54.0",
52
+ "eslint-config-prettier": "^9.0.0",
53
+ "eslint-plugin-playwright": "^0.19.0",
54
+ "husky": "^9.0.6",
55
+ "nodemon": "^3.0.2",
56
+ "prettier": "^3.1.0",
57
+ "ts-node": "^10.9.1",
58
+ "typescript": "^5.3.2",
59
+ "vitest": "^1.0.4"
60
+ },
61
+ "engines": {
62
+ "node": ">=18.0.0"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }
package/release.sh ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # LinkedIn CLI Release Script
4
+ # Usage: ./release.sh [patch|minor|major]
5
+
6
+ set -e
7
+
8
+ BUMP_TYPE="${1:-patch}"
9
+ WORKTREE_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+
11
+ echo "🚀 LinkedIn CLI Release Script"
12
+ echo "=============================="
13
+ echo "Worktree: $WORKTREE_DIR"
14
+ echo "Version bump: $BUMP_TYPE"
15
+ echo ""
16
+
17
+ # Step 1: Run quality checks
18
+ echo "📋 Running quality checks..."
19
+ npm run typecheck || { echo "❌ Type check failed"; exit 1; }
20
+ npm run lint || { echo "❌ Lint failed"; exit 1; }
21
+ npm run format || { echo "❌ Format failed"; exit 1; }
22
+ npm run test:coverage || { echo "❌ Tests failed"; exit 1; }
23
+
24
+ # Step 2: Build
25
+ echo "🔨 Building project..."
26
+ npm run build || { echo "❌ Build failed"; exit 1; }
27
+
28
+ # Step 3: Commit changes
29
+ echo "💾 Committing changes..."
30
+ git add -A
31
+ if git diff-index --quiet HEAD; then
32
+ echo "No changes to commit"
33
+ else
34
+ git commit -m "chore: release preparations (auto-generated)"
35
+ fi
36
+
37
+ # Step 4: Merge to main
38
+ echo "🔄 Merging to main..."
39
+ git checkout main
40
+ git merge linkedin-automation-cli -m "Merge linkedin-automation-cli into main"
41
+
42
+ # Step 5: Push to GitHub
43
+ echo "⬆️ Pushing to GitHub..."
44
+ git push origin main
45
+
46
+ # Step 6: Create version tag
47
+ echo "🏷️ Creating version tag..."
48
+ npm version $BUMP_TYPE --no-git-tag-version
49
+ NEW_VERSION=$(node -p "require('./package.json').version")
50
+ git commit -am "chore: bump version to v$NEW_VERSION"
51
+ git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION"
52
+ git push origin --tags
53
+
54
+ echo ""
55
+ echo "✅ Release complete!"
56
+ echo "📦 Version: v$NEW_VERSION"
57
+ echo "🔗 GitHub: https://github.com/thaddeus-git/linkedin-cli"
58
+ echo "📊 Actions: https://github.com/thaddeus-git/linkedin-cli/actions"
59
+ echo ""
60
+ echo "GitHub Actions is now:"
61
+ echo " 1. Running CI checks"
62
+ echo " 2. Publishing to npm"
63
+ echo " 3. Creating GitHub Release"
64
+ echo ""
65
+ echo "Manual npm publish (if needed):"
66
+ echo " npm login && npm publish --access public"
@@ -0,0 +1,156 @@
1
+ const { chromium } = require('playwright');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const CONFIG_DIR = path.join(require('os').homedir(), '.linkedin-cli');
6
+ const SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
7
+
8
+ async function debugLinkedIn() {
9
+ console.log('🔍 LinkedIn Debug Tool');
10
+ console.log('======================\n');
11
+
12
+ // Load session
13
+ const sessionFile = path.join(SESSIONS_DIR, 'linkedin-session.json');
14
+ if (!fs.existsSync(sessionFile)) {
15
+ console.error('❌ No session found. Run auth login first.');
16
+ return;
17
+ }
18
+
19
+ const encrypted = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
20
+
21
+ // Simple decryption (matching the CLI's method)
22
+ const crypto = require('crypto');
23
+ const machineData = [
24
+ process.env.USER || process.env.USERNAME || 'unknown',
25
+ process.env.HOME || process.env.USERPROFILE || 'unknown',
26
+ process.platform,
27
+ ].join('|');
28
+
29
+ const salt = Buffer.from(encrypted.salt, 'base64');
30
+ const key = crypto.pbkdf2Sync(machineData, salt, 100000, 32, 'sha256');
31
+ const iv = Buffer.from(encrypted.iv, 'base64');
32
+ const encryptedData = Buffer.from(encrypted.encrypted, 'base64');
33
+ const tag = Buffer.from(encrypted.tag, 'base64');
34
+
35
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
36
+ decipher.setAuthTag(tag);
37
+ const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
38
+ const sessionData = JSON.parse(decrypted.toString('utf8'));
39
+
40
+ console.log('✓ Session loaded');
41
+ console.log(` User ID: ${sessionData.cookies.find(c => c.name === 'li_at')?.value?.substring(0, 50)}...`);
42
+
43
+ // Launch browser
44
+ const browser = await chromium.launch({ headless: false });
45
+ const context = await browser.newContext({
46
+ userAgent: sessionData.userAgent,
47
+ viewport: { width: 1280, height: 800 }
48
+ });
49
+
50
+ // Set cookies
51
+ const cookies = sessionData.cookies.map(c => ({
52
+ name: c.name,
53
+ value: c.value,
54
+ domain: c.domain,
55
+ path: c.path || '/',
56
+ secure: c.secure,
57
+ httpOnly: false,
58
+ sameSite: 'Lax'
59
+ }));
60
+
61
+ await context.addCookies(cookies);
62
+ console.log('✓ Cookies set\n');
63
+
64
+ // Try to access feed
65
+ console.log('📰 Checking Feed...');
66
+ const feedPage = await context.newPage();
67
+ await feedPage.goto('https://www.linkedin.com/feed/', { waitUntil: 'domcontentloaded', timeout: 30000 });
68
+ await feedPage.waitForTimeout(3000);
69
+
70
+ const feedTitle = await feedPage.title();
71
+ console.log(` Page title: ${feedTitle}`);
72
+
73
+ // Take screenshot of feed
74
+ await feedPage.screenshot({ path: '/tmp/linkedin-feed.png', fullPage: false });
75
+ console.log(' 📸 Screenshot saved: /tmp/linkedin-feed.png');
76
+
77
+ // Get feed posts
78
+ const posts = await feedPage.$$eval('.feed-shared-update-v2, .scaffold-finite-scroll__content > div', elements =>
79
+ elements.slice(0, 3).map(el => ({
80
+ text: el.innerText?.substring(0, 200),
81
+ author: el.querySelector('[class*="actor"], [class*="author"]')?.innerText
82
+ }))
83
+ );
84
+
85
+ if (posts.length > 0) {
86
+ console.log(` ✓ Found ${posts.length} posts in feed`);
87
+ posts.forEach((post, i) => {
88
+ console.log(`\n Post ${i + 1}:`);
89
+ console.log(` Author: ${post.author || 'Unknown'}`);
90
+ console.log(` Preview: ${post.text?.substring(0, 100)}...`);
91
+ });
92
+ } else {
93
+ console.log(' ⚠ No posts found - checking page structure...');
94
+ }
95
+
96
+ // Try to access messages
97
+ console.log('\n💬 Checking Messages...');
98
+ const msgPage = await context.newPage();
99
+ await msgPage.goto('https://www.linkedin.com/messaging/', { waitUntil: 'domcontentloaded', timeout: 30000 });
100
+ await msgPage.waitForTimeout(3000);
101
+
102
+ const msgTitle = await msgPage.title();
103
+ console.log(` Page title: ${msgTitle}`);
104
+
105
+ // Take screenshot of messages
106
+ await msgPage.screenshot({ path: '/tmp/linkedin-messages.png', fullPage: false });
107
+ console.log(' 📸 Screenshot saved: /tmp/linkedin-messages.png');
108
+
109
+ // Get conversation list
110
+ const conversations = await msgPage.$$eval('.msg-conversation-card, [data-testid="conversation-card"], div[class*="conversation"]', elements =>
111
+ elements.slice(0, 5).map(el => ({
112
+ name: el.querySelector('[class*="participant"], [class*="name"]')?.innerText,
113
+ preview: el.querySelector('[class*="preview"], [class*="message"]')?.innerText,
114
+ unread: el.querySelector('[class*="unread"]') !== null
115
+ }))
116
+ );
117
+
118
+ if (conversations.length > 0) {
119
+ console.log(` ✓ Found ${conversations.length} conversations`);
120
+ conversations.forEach((conv, i) => {
121
+ console.log(`\n Conversation ${i + 1}:`);
122
+ console.log(` From: ${conv.name || 'Unknown'} ${conv.unread ? '(UNREAD)' : ''}`);
123
+ console.log(` Preview: ${conv.preview?.substring(0, 80) || 'No preview'}...`);
124
+ });
125
+ } else {
126
+ console.log(' ⚠ No conversations found');
127
+
128
+ // Debug: list all divs with class names containing 'conversation' or 'message'
129
+ const debugInfo = await msgPage.evaluate(() => {
130
+ const convElements = document.querySelectorAll('div[class*="conversation"], div[class*="message"], div[class*="msg-"]');
131
+ return Array.from(convElements).slice(0, 10).map(el => ({
132
+ className: el.className,
133
+ text: el.innerText?.substring(0, 50)
134
+ }));
135
+ });
136
+
137
+ if (debugInfo.length > 0) {
138
+ console.log(' 🔍 Debug: Found elements with conversation/message classes:');
139
+ debugInfo.forEach((info, i) => {
140
+ console.log(` ${i + 1}. Class: ${info.className?.substring(0, 60)}...`);
141
+ });
142
+ }
143
+ }
144
+
145
+ console.log('\n✅ Debug complete!');
146
+ console.log('\nScreenshots saved:');
147
+ console.log(' - /tmp/linkedin-feed.png');
148
+ console.log(' - /tmp/linkedin-messages.png');
149
+
150
+ await browser.close();
151
+ }
152
+
153
+ debugLinkedIn().catch(error => {
154
+ console.error('❌ Error:', error.message);
155
+ process.exit(1);
156
+ });
@@ -0,0 +1,193 @@
1
+ const { chromium } = require('playwright');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+
6
+ const CONFIG_DIR = path.join(require('os').homedir(), '.linkedin-cli');
7
+ const SCREENSHOT_DIR = path.join(CONFIG_DIR, 'debug');
8
+
9
+ function askQuestion(query) {
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ });
14
+ return new Promise(resolve => rl.question(query, ans => {
15
+ rl.close();
16
+ resolve(ans);
17
+ }));
18
+ }
19
+
20
+
21
+ async function debugLogin() {
22
+ console.log('🔍 LinkedIn Login Debugger');
23
+ console.log('==========================\n');
24
+
25
+ if (!fs.existsSync(SCREENSHOT_DIR)) {
26
+ fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
27
+ }
28
+
29
+ // Launch with maximum stealth
30
+ const browser = await chromium.launch({
31
+ headless: false,
32
+ executablePath: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
33
+ args: [
34
+ '--disable-blink-features=AutomationControlled',
35
+ '--disable-web-security',
36
+ '--disable-features=IsolateOrigins,site-per-process',
37
+ ]
38
+ });
39
+
40
+ const context = await browser.newContext({
41
+ viewport: { width: 1920, height: 1080 },
42
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
43
+ locale: 'en-US',
44
+ timezoneId: 'America/New_York',
45
+ permissions: ['notifications'],
46
+ colorScheme: 'light',
47
+ });
48
+
49
+ // Inject stealth script
50
+ await context.addInitScript(() => {
51
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
52
+ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
53
+ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
54
+ window.chrome = { runtime: {} };
55
+ });
56
+
57
+ const page = await context.newPage();
58
+
59
+ try {
60
+ console.log('1️⃣ Going to LinkedIn login page...');
61
+ await page.goto('https://www.linkedin.com/login', {
62
+ waitUntil: 'networkidle',
63
+ timeout: 60000
64
+ });
65
+
66
+ await page.waitForTimeout(3000);
67
+ await page.screenshot({ path: path.join(SCREENSHOT_DIR, '01-login-page.png') });
68
+ console.log(' 📸 Screenshot saved: 01-login-page.png');
69
+
70
+ console.log('\n2️⃣ Filling email...');
71
+ await page.fill('input#username', '628552@qq.com', { delay: 100 });
72
+ await page.waitForTimeout(1000);
73
+
74
+ console.log('3️⃣ Prompting for password...');
75
+ const password = await askQuestion('Enter your LinkedIn password: ');
76
+ console.log(' (Password received, filling form...)');
77
+ await page.fill('input#password', password, { delay: 100 });
78
+ await page.fill('input#password', process.env.LINKEDIN_PASSWORD || '', { delay: 100 });
79
+ await page.waitForTimeout(1000);
80
+
81
+ console.log('4️⃣ Clicking submit...');
82
+ await page.click('button[type="submit"]');
83
+
84
+ console.log('5️⃣ Waiting for navigation...');
85
+ await page.waitForTimeout(5000);
86
+
87
+ await page.screenshot({ path: path.join(SCREENSHOT_DIR, '02-after-submit.png') });
88
+ console.log(' 📸 Screenshot saved: 02-after-submit.png');
89
+
90
+ // Check current URL
91
+ const currentUrl = page.url();
92
+ console.log(`\n📍 Current URL: ${currentUrl}`);
93
+
94
+ // Check if we're on 2FA page
95
+ const has2FA = await page.$('input#input__phone_verification_pin') !== null;
96
+ const hasCaptcha = await page.$('[data-testid="captcha-internal"]') !== null ||
97
+ await page.$('.captcha') !== null ||
98
+ await page.$('text=Security verification') !== null;
99
+
100
+ if (hasCaptcha) {
101
+ console.log('\n🚨 CAPTCHA DETECTED!');
102
+ console.log(' LinkedIn is showing a security challenge.');
103
+ console.log(' You need to manually solve it in the browser window.');
104
+ await page.screenshot({ path: path.join(SCREENSHOT_DIR, '03-captcha.png') });
105
+
106
+ console.log('\n⏳ Waiting 60 seconds for you to solve CAPTCHA...');
107
+ await page.waitForTimeout(60000);
108
+ }
109
+
110
+ if (has2FA) {
111
+ console.log('\n🔐 2FA Prompt Detected');
112
+ console.log(' Please enter the 6-digit code from Microsoft Authenticator');
113
+
114
+ // Wait for manual 2FA entry
115
+ await page.waitForTimeout(15000);
116
+
117
+ await page.screenshot({ path: path.join(SCREENSHOT_DIR, '03-2fa.png') });
118
+ console.log(' 📸 Screenshot saved: 03-2fa.png');
119
+ }
120
+
121
+ // Check if we're logged in
122
+ const isLoggedIn = await page.$('header.global-nav') !== null ||
123
+ await page.$('[data-testid="global-nav"]') !== null;
124
+
125
+ if (isLoggedIn) {
126
+ console.log('\n✅ SUCCESSFULLY LOGGED IN!');
127
+
128
+ // Save session
129
+ const cookies = await context.cookies();
130
+ const sessionData = {
131
+ cookies,
132
+ timestamp: Date.now(),
133
+ userAgent: await page.evaluate(() => navigator.userAgent)
134
+ };
135
+
136
+ // Encrypt and save (simplified)
137
+ const sessionFile = path.join(CONFIG_DIR, 'sessions', 'linkedin-session.json');
138
+ fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
139
+
140
+ // Use same encryption as CLI
141
+ const crypto = require('crypto');
142
+ const machineData = [process.env.USER, process.env.HOME, process.platform].join('|');
143
+ const salt = crypto.randomBytes(32);
144
+ const key = crypto.pbkdf2Sync(machineData, salt, 100000, 32, 'sha256');
145
+ const iv = crypto.randomBytes(16);
146
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
147
+ const encrypted = Buffer.concat([cipher.update(JSON.stringify(sessionData)), cipher.final()]);
148
+ const tag = cipher.getAuthTag();
149
+
150
+ fs.writeFileSync(sessionFile, JSON.stringify({
151
+ encrypted: encrypted.toString('base64'),
152
+ iv: iv.toString('base64'),
153
+ salt: salt.toString('base64'),
154
+ tag: tag.toString('base64')
155
+ }, null, 2));
156
+
157
+ console.log(` 💾 Session saved to: ${sessionFile}`);
158
+
159
+ // Test feed access
160
+ console.log('\n6️⃣ Testing feed access...');
161
+ await page.goto('https://www.linkedin.com/feed/', { waitUntil: 'networkidle' });
162
+ await page.waitForTimeout(3000);
163
+ await page.screenshot({ path: path.join(SCREENSHOT_DIR, '04-feed.png') });
164
+ console.log(' 📸 Screenshot saved: 04-feed.png');
165
+ console.log(` 📄 Page title: ${await page.title()}`);
166
+
167
+ } else {
168
+ console.log('\n❌ Login verification failed');
169
+ await page.screenshot({ path: path.join(SCREENSHOT_DIR, '03-failed.png') });
170
+ console.log(' 📸 Screenshot saved: 03-failed.png');
171
+
172
+ // Get page content for debugging
173
+ const content = await page.content();
174
+ fs.writeFileSync(path.join(SCREENSHOT_DIR, 'page-content.html'), content);
175
+ console.log(' 📝 Page HTML saved: page-content.html');
176
+ }
177
+
178
+ console.log(`\n📁 All debug files saved to: ${SCREENSHOT_DIR}`);
179
+ console.log('\nBrowser will stay open for 30 seconds...');
180
+ await page.waitForTimeout(30000);
181
+
182
+ } catch (error) {
183
+ console.error('❌ Error:', error.message);
184
+ await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'error.png') });
185
+ }
186
+
187
+ await browser.close();
188
+ }
189
+
190
+ console.log('This script will help debug the LinkedIn login process.');
191
+ console.log('It will open a visible browser and take screenshots at each step.\n');
192
+
193
+ debugLogin().catch(console.error);