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,1592 @@
1
+ # LinkedIn CLI Open Source Publishing Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Prepare the LinkedIn CLI for open source publication with comprehensive testing, OSS metadata, CI/CD, and documentation.
6
+
7
+ **Architecture:** Three-phase approach: (1) Critical items for basic publishing, (2) Important items for quality, (3) Nice-to-have improvements. Each phase builds incrementally with tests first.
8
+
9
+ **Tech Stack:** Go 1.21+, Cobra CLI, PinchTab HTTP client, GitHub Actions, Go testing package.
10
+
11
+ ---
12
+
13
+ ## Phase 1: Critical (Must Have)
14
+
15
+ ### Task 1: Add MIT LICENSE File
16
+
17
+ **Files:**
18
+ - Create: `LICENSE`
19
+
20
+ **Step 1: Write LICENSE file**
21
+
22
+ ```
23
+ MIT License
24
+
25
+ Copyright (c) 2026 Thaddeus Liu
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
44
+ ```
45
+
46
+ **Step 2: Commit**
47
+
48
+ ```bash
49
+ git add LICENSE
50
+ git commit -m "docs: add MIT license for open source distribution"
51
+ ```
52
+
53
+ ---
54
+
55
+ ### Task 2: Add URL Validator Tests
56
+
57
+ **Files:**
58
+ - Create: `internal/linkedin/validator_test.go`
59
+ - Reference: `internal/linkedin/validator.go`
60
+
61
+ **Step 1: Write validator tests**
62
+
63
+ ```go
64
+ package linkedin
65
+
66
+ import "testing"
67
+
68
+ func TestValidateProfileURL(t *testing.T) {
69
+ tests := []struct {
70
+ name string
71
+ input string
72
+ want string
73
+ wantErr bool
74
+ }{
75
+ {
76
+ name: "full URL with https",
77
+ input: "https://linkedin.com/in/john-doe",
78
+ want: "https://linkedin.com/in/john-doe",
79
+ },
80
+ {
81
+ name: "full URL with http",
82
+ input: "http://linkedin.com/in/john-doe",
83
+ want: "http://linkedin.com/in/john-doe",
84
+ },
85
+ {
86
+ name: "vanity URL without protocol",
87
+ input: "linkedin.com/in/john-doe",
88
+ want: "https://linkedin.com/in/john-doe",
89
+ },
90
+ {
91
+ name: "username only",
92
+ input: "john-doe",
93
+ want: "https://linkedin.com/in/john-doe",
94
+ },
95
+ {
96
+ name: "non-LinkedIn URL",
97
+ input: "https://example.com/john",
98
+ wantErr: true,
99
+ },
100
+ {
101
+ name: "URL without /in/",
102
+ input: "https://linkedin.com/company/example",
103
+ wantErr: true,
104
+ },
105
+ {
106
+ name: "empty input",
107
+ input: "",
108
+ wantErr: true,
109
+ },
110
+ }
111
+
112
+ for _, tt := range tests {
113
+ t.Run(tt.name, func(t *testing.T) {
114
+ got, err := ValidateProfileURL(tt.input)
115
+ if (err != nil) != tt.wantErr {
116
+ t.Errorf("ValidateProfileURL() error = %v, wantErr %v", err, tt.wantErr)
117
+ return
118
+ }
119
+ if got != tt.want {
120
+ t.Errorf("ValidateProfileURL() = %v, want %v", got, tt.want)
121
+ }
122
+ })
123
+ }
124
+ }
125
+
126
+ func TestExtractProfileUsername(t *testing.T) {
127
+ tests := []struct {
128
+ name string
129
+ url string
130
+ want string
131
+ }{
132
+ {
133
+ name: "full URL",
134
+ url: "https://linkedin.com/in/john-doe",
135
+ want: "john-doe",
136
+ },
137
+ {
138
+ name: "URL with trailing slash",
139
+ url: "https://linkedin.com/in/john-doe/",
140
+ want: "john-doe",
141
+ },
142
+ {
143
+ name: "invalid URL",
144
+ url: "not-a-url",
145
+ want: "",
146
+ },
147
+ }
148
+
149
+ for _, tt := range tests {
150
+ t.Run(tt.name, func(t *testing.T) {
151
+ got := ExtractProfileUsername(tt.url)
152
+ if got != tt.want {
153
+ t.Errorf("ExtractProfileUsername() = %v, want %v", got, tt.want)
154
+ }
155
+ })
156
+ }
157
+ }
158
+ ```
159
+
160
+ **Step 2: Run test to verify it passes**
161
+
162
+ ```bash
163
+ go test -v ./internal/linkedin/...
164
+ ```
165
+
166
+ Expected: PASS
167
+
168
+ **Step 3: Commit**
169
+
170
+ ```bash
171
+ git add internal/linkedin/validator_test.go
172
+ git commit -m "test: add URL validator tests"
173
+ ```
174
+
175
+ ---
176
+
177
+ ### Task 3: Add Rate Limiter Tests
178
+
179
+ **Files:**
180
+ - Create: `internal/ratelimit/limiter_test.go`
181
+ - Reference: `internal/ratelimit/limiter.go`
182
+
183
+ **Step 1: Write limiter tests**
184
+
185
+ ```go
186
+ package ratelimit
187
+
188
+ import (
189
+ "testing"
190
+ "time"
191
+
192
+ "github.com/thaddeus-git/linkedin-cli/internal/config"
193
+ )
194
+
195
+ func TestLimiterCheckConnection(t *testing.T) {
196
+ tempDir := t.TempDir()
197
+ mgr := &config.Manager{}
198
+ // Inject temp dir via reflection or helper
199
+
200
+ limits := DefaultLimits()
201
+ limiter := NewLimiter("test", limits, mgr)
202
+
203
+ // Test under limit
204
+ err := limiter.CheckConnection()
205
+ if err != nil {
206
+ t.Errorf("expected no error when under limit, got %v", err)
207
+ }
208
+ }
209
+
210
+ func TestLimiterCheckConnectionExceeded(t *testing.T) {
211
+ limits := Limits{
212
+ ConnectionsDaily: 20,
213
+ ConnectionsWeekly: 100,
214
+ }
215
+
216
+ state := &config.RateLimits{
217
+ Connections: config.RateLimit{
218
+ Today: 20,
219
+ ThisWeek: 100,
220
+ LastAction: time.Now(),
221
+ },
222
+ }
223
+
224
+ if state.CanConnect(limits.ConnectionsDaily, limits.ConnectionsWeekly) {
225
+ t.Error("should not allow connection when at limit")
226
+ }
227
+ }
228
+
229
+ func TestLimiterRecordConnection(t *testing.T) {
230
+ tempDir := t.TempDir()
231
+ mgr := &config.Manager{}
232
+
233
+ limiter := NewLimiter("test", DefaultLimits(), mgr)
234
+
235
+ // Record connection
236
+ err := limiter.RecordConnection()
237
+ if err != nil {
238
+ t.Fatalf("failed to record connection: %v", err)
239
+ }
240
+
241
+ // Verify recorded
242
+ loaded, err := mgr.LoadRateLimits("test")
243
+ if err != nil {
244
+ t.Fatalf("failed to load rate limits: %v", err)
245
+ }
246
+
247
+ if loaded.Connections.Today != 1 {
248
+ t.Errorf("expected 1 connection recorded, got %d", loaded.Connections.Today)
249
+ }
250
+ }
251
+
252
+ func TestLimiterGetDelay(t *testing.T) {
253
+ limiter := NewLimiter("test", DefaultLimits(), nil)
254
+
255
+ delay := limiter.GetDelay()
256
+ minDelay := time.Duration(DefaultLimits().MinDelaySeconds) * time.Second
257
+ maxDelay := time.Duration(DefaultLimits().MaxDelaySeconds) * time.Second
258
+
259
+ if delay < minDelay || delay > maxDelay {
260
+ t.Errorf("delay %v outside expected range [%v, %v]", delay, minDelay, maxDelay)
261
+ }
262
+ }
263
+ ```
264
+
265
+ **Step 2: Run test to verify it passes**
266
+
267
+ ```bash
268
+ go test -v ./internal/ratelimit/...
269
+ ```
270
+
271
+ Expected: PASS (may need to fix config manager injection)
272
+
273
+ **Step 3: Commit**
274
+
275
+ ```bash
276
+ git add internal/ratelimit/limiter_test.go
277
+ git commit -m "test: add rate limiter tests"
278
+ ```
279
+
280
+ ---
281
+
282
+ ### Task 4: Add Limits Tests
283
+
284
+ **Files:**
285
+ - Create: `internal/ratelimit/limits_test.go`
286
+
287
+ **Step 1: Write limits tests**
288
+
289
+ ```go
290
+ package ratelimit
291
+
292
+ import "testing"
293
+
294
+ func TestDefaultLimits(t *testing.T) {
295
+ limits := DefaultLimits()
296
+
297
+ if limits.ConnectionsDaily != 20 {
298
+ t.Errorf("expected 20 daily connections, got %d", limits.ConnectionsDaily)
299
+ }
300
+
301
+ if limits.ConnectionsWeekly != 100 {
302
+ t.Errorf("expected 100 weekly connections, got %d", limits.ConnectionsWeekly)
303
+ }
304
+
305
+ if limits.MessagesDaily != 50 {
306
+ t.Errorf("expected 50 daily messages, got %d", limits.MessagesDaily)
307
+ }
308
+
309
+ if limits.MinDelaySeconds != 3 {
310
+ t.Errorf("expected 3s min delay, got %d", limits.MinDelaySeconds)
311
+ }
312
+
313
+ if limits.MaxDelaySeconds != 8 {
314
+ t.Errorf("expected 8s max delay, got %d", limits.MaxDelaySeconds)
315
+ }
316
+ }
317
+
318
+ func TestConservativeLimits(t *testing.T) {
319
+ limits := ConservativeLimits()
320
+
321
+ if limits.ConnectionsDaily != 10 {
322
+ t.Errorf("expected 10 daily connections, got %d", limits.ConnectionsDaily)
323
+ }
324
+
325
+ if limits.ConnectionsWeekly != 50 {
326
+ t.Errorf("expected 50 weekly connections, got %d", limits.ConnectionsWeekly)
327
+ }
328
+
329
+ if limits.MessagesDaily != 25 {
330
+ t.Errorf("expected 25 daily messages, got %d", limits.MessagesDaily)
331
+ }
332
+
333
+ if limits.MinDelaySeconds != 5 {
334
+ t.Errorf("expected 5s min delay, got %d", limits.MinDelaySeconds)
335
+ }
336
+
337
+ if limits.MaxDelaySeconds != 12 {
338
+ t.Errorf("expected 12s max delay, got %d", limits.MaxDelaySeconds)
339
+ }
340
+ }
341
+ ```
342
+
343
+ **Step 2: Run test to verify it passes**
344
+
345
+ ```bash
346
+ go test -v ./internal/ratelimit/...
347
+ ```
348
+
349
+ Expected: PASS
350
+
351
+ **Step 3: Commit**
352
+
353
+ ```bash
354
+ git add internal/ratelimit/limits_test.go
355
+ git commit -m "test: add limits configuration tests"
356
+ ```
357
+
358
+ ---
359
+
360
+ ### Task 5: Add Version Flag
361
+
362
+ **Files:**
363
+ - Modify: `cmd/linkedin/main.go`
364
+ - Modify: `internal/cmd/root.go`
365
+ - Modify: `Makefile`
366
+
367
+ **Step 1: Modify main.go to add version variable**
368
+
369
+ ```go
370
+ package main
371
+
372
+ import (
373
+ "github.com/thaddeus-git/linkedin-cli/internal/cmd"
374
+ )
375
+
376
+ var Version = "dev"
377
+
378
+ func main() {
379
+ cmd.Version = Version
380
+ cmd.Execute()
381
+ }
382
+ ```
383
+
384
+ **Step 2: Modify root.go to add version flag**
385
+
386
+ ```go
387
+ var rootCmd = &cobra.Command{
388
+ Use: "linkedin",
389
+ Short: "LinkedIn automation CLI",
390
+ // ... existing Long ...
391
+ }
392
+
393
+ func init() {
394
+ // ... existing flags ...
395
+
396
+ cobra.OnInitialize(initConfig)
397
+ rootCmd.Version = "dev" // Will be set by main
398
+ }
399
+ ```
400
+
401
+ **Step 3: Modify Makefile to build with version**
402
+
403
+ ```makefile
404
+ VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
405
+
406
+ build:
407
+ go build -ldflags "-X main.Version=$(VERSION)" -o $(BINARY_NAME) $(MAIN_PACKAGE)
408
+ ```
409
+
410
+ **Step 4: Test version command**
411
+
412
+ ```bash
413
+ make build
414
+ ./linkedin-cli --version
415
+ ```
416
+
417
+ Expected: Output like `linkedin-cli version 0.1.0` or `linkedin-cli version dev`
418
+
419
+ **Step 5: Commit**
420
+
421
+ ```bash
422
+ git add cmd/linkedin/main.go internal/cmd/root.go Makefile
423
+ git commit -m "feat: add version flag"
424
+ ```
425
+
426
+ ---
427
+
428
+ ### Task 6: Run All Tests and Verify Coverage
429
+
430
+ **Step 1: Run all tests**
431
+
432
+ ```bash
433
+ go test -v ./...
434
+ ```
435
+
436
+ Expected: All tests PASS
437
+
438
+ **Step 2: Check coverage**
439
+
440
+ ```bash
441
+ go test -coverprofile=coverage.out ./...
442
+ go tool cover -func=coverage.out
443
+ ```
444
+
445
+ Expected: See coverage percentages per package
446
+
447
+ **Step 3: Commit test results**
448
+
449
+ ```bash
450
+ git add coverage.out
451
+ git commit -m "test: verify all tests passing with coverage"
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Phase 2: Important (Should Have)
457
+
458
+ ### Task 7: Create CONTRIBUTING.md
459
+
460
+ **Files:**
461
+ - Create: `CONTRIBUTING.md`
462
+
463
+ **Step 1: Write CONTRIBUTING.md**
464
+
465
+ ```markdown
466
+ # Contributing to LinkedIn CLI
467
+
468
+ Thank you for considering contributing to LinkedIn CLI! This guide will help you get started.
469
+
470
+ ## Development Setup
471
+
472
+ ### Prerequisites
473
+
474
+ - Go 1.21 or higher
475
+ - PinchTab installed (`curl -fsSL https://pinchtab.com/install.sh | bash`)
476
+
477
+ ### Installation
478
+
479
+ ```bash
480
+ git clone https://github.com/thaddeus-git/linkedin-cli.git
481
+ cd linkedin-cli
482
+ go build -o linkedin-cli ./cmd/linkedin
483
+ ```
484
+
485
+ ### Running Tests
486
+
487
+ ```bash
488
+ go test -v ./...
489
+ ```
490
+
491
+ ### Running Linter
492
+
493
+ ```bash
494
+ make lint
495
+ ```
496
+
497
+ ## Code Style
498
+
499
+ - Follow Go best practices
500
+ - Use `go fmt` and `go vet`
501
+ - Write tests for new functionality
502
+ - Keep functions small and focused
503
+
504
+ ## Pull Request Process
505
+
506
+ 1. Fork the repository
507
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
508
+ 3. Make your changes
509
+ 4. Run tests and ensure they pass
510
+ 5. Commit with clear messages
511
+ 6. Push and open a PR
512
+
513
+ ## Issue Reporting
514
+
515
+ ### Bug Reports
516
+
517
+ Include:
518
+ - Steps to reproduce
519
+ - Expected behavior
520
+ - Actual behavior
521
+ - Environment (OS, Go version)
522
+
523
+ ### Feature Requests
524
+
525
+ Include:
526
+ - Problem you're trying to solve
527
+ - Proposed solution
528
+ - Alternative approaches considered
529
+
530
+ ## Questions?
531
+
532
+ Open an issue for any questions about the codebase or contribution process.
533
+ ```
534
+
535
+ **Step 2: Commit**
536
+
537
+ ```bash
538
+ git add CONTRIBUTING.md
539
+ git commit -m "docs: add contributing guide"
540
+ ```
541
+
542
+ ---
543
+
544
+ ### Task 8: Create GitHub Issue Templates
545
+
546
+ **Files:**
547
+ - Create: `.github/ISSUE_TEMPLATE/bug_report.md`
548
+ - Create: `.github/ISSUE_TEMPLATE/feature_request.md`
549
+
550
+ **Step 1: Write bug report template**
551
+
552
+ ```markdown
553
+ ---
554
+ name: Bug report
555
+ about: Create a report to help us improve
556
+ title: ''
557
+ labels: bug
558
+ assignees: ''
559
+
560
+ ---
561
+
562
+ ## Describe the bug
563
+
564
+ A clear and concise description of what the bug is.
565
+
566
+ ## To Reproduce
567
+
568
+ Steps to reproduce the behavior:
569
+ 1. Run command '...'
570
+ 2. With options '...'
571
+ 3. See error
572
+
573
+ ## Expected behavior
574
+
575
+ A clear and concise description of what you expected to happen.
576
+
577
+ ## Actual behavior
578
+
579
+ What actually happened.
580
+
581
+ ## Environment
582
+
583
+ - OS: [e.g., macOS 14.0, Ubuntu 22.04]
584
+ - Go version: [e.g., 1.21.0]
585
+ - LinkedIn CLI version: [e.g., 0.1.0]
586
+
587
+ ## Additional context
588
+
589
+ Add any other context about the problem here.
590
+ ```
591
+
592
+ **Step 2: Write feature request template**
593
+
594
+ ```markdown
595
+ ---
596
+ name: Feature request
597
+ about: Suggest an idea for this project
598
+ title: ''
599
+ labels: enhancement
600
+ assignees: ''
601
+
602
+ ---
603
+
604
+ ## Problem
605
+
606
+ What problem are you trying to solve?
607
+
608
+ ## Proposed Solution
609
+
610
+ Describe the solution you'd like.
611
+
612
+ ## Alternative Approaches
613
+
614
+ Describe any alternative solutions or features you've considered.
615
+
616
+ ## Additional Context
617
+
618
+ Add any other context or examples about the feature request here.
619
+ ```
620
+
621
+ **Step 3: Commit**
622
+
623
+ ```bash
624
+ git add .github/ISSUE_TEMPLATE/
625
+ git commit -m "docs: add issue templates for bugs and features"
626
+ ```
627
+
628
+ ---
629
+
630
+ ### Task 9: Create PR Template
631
+
632
+ **Files:**
633
+ - Create: `.github/PULL_REQUEST_TEMPLATE.md`
634
+
635
+ **Step 1: Write PR template**
636
+
637
+ ```markdown
638
+ ## Description
639
+
640
+ Brief description of the changes in this PR.
641
+
642
+ ## Related Issue
643
+
644
+ Link to the related issue (if applicable).
645
+
646
+ ## Type of Change
647
+
648
+ - [ ] Bug fix (non-breaking change which fixes an issue)
649
+ - [ ] New feature (non-breaking change which adds functionality)
650
+ - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
651
+ - [ ] Documentation update
652
+ - [ ] Code quality improvements
653
+
654
+ ## Testing
655
+
656
+ - [ ] Tests pass locally (`go test ./...`)
657
+ - [ ] Code is formatted (`go fmt ./...`)
658
+ - [ ] New tests added for new functionality
659
+
660
+ ## Checklist
661
+
662
+ - [ ] My code follows the style guidelines of this project
663
+ - [ ] I have performed a self-review of my own code
664
+ - [ ] I have commented my code, particularly in hard-to-understand areas
665
+ - [ ] I have made corresponding changes to the documentation
666
+ - [ ] My changes generate no new warnings
667
+ - [ ] New and existing tests pass locally with my changes
668
+
669
+ ## Additional Notes
670
+
671
+ Any additional information for reviewers.
672
+ ```
673
+
674
+ **Step 2: Commit**
675
+
676
+ ```bash
677
+ git add .github/PULL_REQUEST_TEMPLATE.md
678
+ git commit -m "docs: add pull request template"
679
+ ```
680
+
681
+ ---
682
+
683
+ ### Task 10: Create CI/CD Workflow
684
+
685
+ **Files:**
686
+ - Create: `.github/workflows/ci.yml`
687
+
688
+ **Step 1: Write CI workflow**
689
+
690
+ ```yaml
691
+ name: CI
692
+
693
+ on:
694
+ push:
695
+ branches: [main]
696
+ pull_request:
697
+ branches: [main]
698
+
699
+ jobs:
700
+ test:
701
+ name: Test
702
+ runs-on: ubuntu-latest
703
+ steps:
704
+ - name: Checkout code
705
+ uses: actions/checkout@v4
706
+
707
+ - name: Set up Go
708
+ uses: actions/setup-go@v5
709
+ with:
710
+ go-version: '1.21'
711
+
712
+ - name: Install dependencies
713
+ run: go mod download
714
+
715
+ - name: Run tests
716
+ run: go test -v -race ./...
717
+
718
+ - name: Run tests with coverage
719
+ run: go test -coverprofile=coverage.out ./...
720
+
721
+ - name: Upload coverage to Codecov
722
+ uses: codecov/codecov-action@v4
723
+ with:
724
+ file: ./coverage.out
725
+ flags: unittests
726
+
727
+ build:
728
+ name: Build
729
+ runs-on: ${{ matrix.os }}
730
+ strategy:
731
+ matrix:
732
+ os: [ubuntu-latest, macos-latest, windows-latest]
733
+ arch: [amd64, arm64]
734
+ exclude:
735
+ - os: windows-latest
736
+ arch: arm64
737
+ steps:
738
+ - name: Checkout code
739
+ uses: actions/checkout@v4
740
+
741
+ - name: Set up Go
742
+ uses: actions/setup-go@v5
743
+ with:
744
+ go-version: '1.21'
745
+
746
+ - name: Build
747
+ run: go build -o linkedin-cli ./cmd/linkedin
748
+ env:
749
+ GOOS: ${{ matrix.os == 'windows-latest' && 'windows' || (matrix.os == 'macos-latest' && 'darwin' || 'linux') }}
750
+ GOARCH: ${{ matrix.arch }}
751
+
752
+ lint:
753
+ name: Lint
754
+ runs-on: ubuntu-latest
755
+ steps:
756
+ - name: Checkout code
757
+ uses: actions/checkout@v4
758
+
759
+ - name: Set up Go
760
+ uses: actions/setup-go@v5
761
+ with:
762
+ go-version: '1.21'
763
+
764
+ - name: Install golangci-lint
765
+ uses: golangci/golangci-lint-action@v4
766
+ with:
767
+ version: latest
768
+ ```
769
+
770
+ **Step 2: Commit**
771
+
772
+ ```bash
773
+ git add .github/workflows/ci.yml
774
+ git commit -m "ci: add GitHub Actions CI workflow"
775
+ ```
776
+
777
+ ---
778
+
779
+ ### Task 11: Add Structured Logging
780
+
781
+ **Files:**
782
+ - Modify: `internal/cmd/root.go`
783
+ - Modify: `internal/cmd/auth.go`
784
+ - Modify: `internal/cmd/connect.go`
785
+ - Modify: `internal/cmd/message.go`
786
+
787
+ **Step 1: Add slog to root.go**
788
+
789
+ ```go
790
+ import (
791
+ "log/slog"
792
+ "os"
793
+ // ... existing imports
794
+ )
795
+
796
+ var logger *slog.Logger
797
+
798
+ func init() {
799
+ // ... existing init ...
800
+
801
+ // Initialize logger
802
+ level := slog.LevelInfo
803
+ if verbose {
804
+ level = slog.LevelDebug
805
+ }
806
+
807
+ opts := &slog.HandlerOptions{Level: level}
808
+ logger = slog.New(slog.NewTextHandler(os.Stdout, opts))
809
+ }
810
+ ```
811
+
812
+ **Step 2: Update verbose logging in root.go**
813
+
814
+ ```go
815
+ // logVerbose prints if verbose mode is enabled
816
+ func logVerbose(format string, args ...interface{}) {
817
+ logger.Debug(fmt.Sprintf(format, args...))
818
+ }
819
+ ```
820
+
821
+ **Step 3: Update auth.go**
822
+
823
+ ```go
824
+ // Before
825
+ fmt.Printf("Starting authentication for profile '%s'...\n", profileName)
826
+ logVerbose("Using PinchTab at %s", getPinchTabHost())
827
+
828
+ // After
829
+ logger.Info("starting authentication", "profile", profileName)
830
+ logger.Debug("pinchtab host", "host", getPinchTabHost())
831
+ ```
832
+
833
+ **Step 4: Test logging**
834
+
835
+ ```bash
836
+ make build
837
+ ./linkedin-cli auth --profile test --verbose
838
+ ```
839
+
840
+ Expected: See debug logs with `[DEBUG]` prefix
841
+
842
+ **Step 5: Commit**
843
+
844
+ ```bash
845
+ git add internal/cmd/*.go
846
+ git commit -m "feat: add structured logging with slog"
847
+ ```
848
+
849
+ ---
850
+
851
+ ### Task 12: Add Rate Limit Documentation
852
+
853
+ **Files:**
854
+ - Create: `docs/RATE_LIMITS.md`
855
+
856
+ **Step 1: Write rate limits documentation**
857
+
858
+ ```markdown
859
+ # Rate Limits
860
+
861
+ LinkedIn CLI includes built-in rate limiting to keep your LinkedIn account safe from automated detection.
862
+
863
+ ## Default Limits
864
+
865
+ | Action | Daily Limit | Weekly Limit |
866
+ |--------|-------------|--------------|
867
+ | Connection requests | 20 | 100 |
868
+ | Direct messages | 50 | N/A |
869
+
870
+ ## Delays
871
+
872
+ Between actions, LinkedIn CLI waits a random duration:
873
+
874
+ - **Default:** 3-8 seconds
875
+ - **Conservative mode:** 5-12 seconds
876
+
877
+ ## Conservative Mode
878
+
879
+ For new LinkedIn accounts (< 3 months), use conservative limits:
880
+
881
+ ```bash
882
+ linkedin connect --profile new-account --conservative
883
+ ```
884
+
885
+ This applies:
886
+ - 10 connections/day (instead of 20)
887
+ - 50 connections/week (instead of 100)
888
+ - 25 messages/day (instead of 50)
889
+ - 5-12 second delays (instead of 3-8)
890
+
891
+ ## How It Works
892
+
893
+ 1. **Tracking:** Actions are tracked per profile in `~/.linkedin-cli/ratelimit.json`
894
+ 2. **Reset:** Daily counters reset at midnight (local time)
895
+ 3. **Reset:** Weekly counters reset on Monday (ISO week)
896
+ 4. **Blocking:** Commands fail with error if limit exceeded
897
+
898
+ ## Viewing Rate Limits
899
+
900
+ ```bash
901
+ linkedin profiles list
902
+ ```
903
+
904
+ Shows rate limit status for each profile.
905
+
906
+ ## Safety Tips
907
+
908
+ 1. **Don't increase limits** - LinkedIn's detection is sophisticated
909
+ 2. **Use multiple profiles** - Distribute automation across accounts
910
+ 3. **Monitor account health** - Watch for LinkedIn warnings
911
+ 4. **Start conservative** - Begin with low volume, increase gradually
912
+
913
+ ## Bypassing Limits (Not Recommended)
914
+
915
+ You can bypass limits with `--no-rate-limit` flag, but this risks:
916
+ - LinkedIn account restrictions
917
+ - Permanent bans
918
+ - Detection as automated behavior
919
+
920
+ **Use at your own risk.**
921
+
922
+ ## LinkedIn's Actual Limits
923
+
924
+ LinkedIn doesn't publish exact limits, but community observations suggest:
925
+ - ~100 connection requests/week for established accounts
926
+ - Lower limits for new accounts
927
+ - Additional behavioral detection (mouse movement, timing patterns)
928
+
929
+ LinkedIn CLI uses conservative defaults based on community feedback.
930
+ ```
931
+
932
+ **Step 2: Commit**
933
+
934
+ ```bash
935
+ git add docs/RATE_LIMITS.md
936
+ git commit -m "docs: add rate limits documentation"
937
+ ```
938
+
939
+ ---
940
+
941
+ ## Phase 3: Nice to Have
942
+
943
+ ### Task 13: Add Testable Time Handling
944
+
945
+ **Files:**
946
+ - Modify: `internal/linkedin/navigator.go`
947
+ - Create: `internal/linkedin/sleeper.go`
948
+ - Modify: `internal/linkedin/navigator_test.go` (from Task 14)
949
+
950
+ **Step 1: Create sleeper interface**
951
+
952
+ ```go
953
+ package linkedin
954
+
955
+ import "time"
956
+
957
+ // Sleeper abstracts time.Sleep for testability
958
+ type Sleeper interface {
959
+ Sleep(time.Duration)
960
+ }
961
+
962
+ // RealSleeper uses actual time.Sleep
963
+ type RealSleeper struct{}
964
+
965
+ func (RealSleeper) Sleep(d time.Duration) {
966
+ time.Sleep(d)
967
+ }
968
+
969
+ // NoOpSleeper is used in tests
970
+ type NoOpSleeper struct{}
971
+
972
+ func (NoOpSleeper) Sleep(time.Duration) {}
973
+ ```
974
+
975
+ **Step 2: Update Navigator struct**
976
+
977
+ ```go
978
+ type Navigator struct {
979
+ client *pinchtab.Client
980
+ sleeper Sleeper
981
+ }
982
+
983
+ func NewNavigator(client *pinchtab.Client) *Navigator {
984
+ return &Navigator{
985
+ client: client,
986
+ sleeper: RealSleeper{},
987
+ }
988
+ }
989
+ ```
990
+
991
+ **Step 3: Update navigation methods**
992
+
993
+ ```go
994
+ // Before
995
+ time.Sleep(3 * time.Second)
996
+
997
+ // After
998
+ n.sleeper.Sleep(3 * time.Second)
999
+ ```
1000
+
1001
+ **Step 4: Commit**
1002
+
1003
+ ```bash
1004
+ git add internal/linkedin/sleeper.go internal/linkedin/navigator.go
1005
+ git commit -m "refactor: add testable sleeper interface"
1006
+ ```
1007
+
1008
+ ---
1009
+
1010
+ ### Task 14: Add Navigator Tests
1011
+
1012
+ **Files:**
1013
+ - Create: `internal/linkedin/navigator_test.go`
1014
+ - Create: `internal/linkedin/client_mock.go`
1015
+
1016
+ **Step 1: Create mock client**
1017
+
1018
+ ```go
1019
+ package linkedin
1020
+
1021
+ import "github.com/thaddeus-git/linkedin-cli/internal/pinchtab"
1022
+
1023
+ // MockPinchTabClient for testing
1024
+ type MockPinchTabClient struct {
1025
+ SnapshotFunc func(filter string) (*pinchtab.Snapshot, error)
1026
+ NavigateFunc func(url string) error
1027
+ ClickFunc func(ref string) error
1028
+ TypeFunc func(ref string, text string) error
1029
+ GetTextFunc func() (*pinchtab.TextResponse, error)
1030
+ CalledMethods map[string]int
1031
+ }
1032
+
1033
+ func NewMockClient() *MockPinchTabClient {
1034
+ return &MockPinchTabClient{
1035
+ CalledMethods: make(map[string]int),
1036
+ }
1037
+ }
1038
+
1039
+ func (m *MockPinchTabClient) Navigate(url string) error {
1040
+ m.CalledMethods["Navigate"]++
1041
+ if m.NavigateFunc != nil {
1042
+ return m.NavigateFunc(url)
1043
+ }
1044
+ return nil
1045
+ }
1046
+
1047
+ func (m *MockPinchTabClient) GetSnapshot(filter string) (*pinchtab.Snapshot, error) {
1048
+ m.CalledMethods["GetSnapshot"]++
1049
+ if m.SnapshotFunc != nil {
1050
+ return m.SnapshotFunc(filter)
1051
+ }
1052
+ return &pinchtab.Snapshot{}, nil
1053
+ }
1054
+
1055
+ func (m *MockPinchTabClient) HumanClick(ref string) error {
1056
+ m.CalledMethods["HumanClick"]++
1057
+ if m.ClickFunc != nil {
1058
+ return m.ClickFunc(ref)
1059
+ }
1060
+ return nil
1061
+ }
1062
+
1063
+ func (m *MockPinchTabClient) HumanType(ref string, text string) error {
1064
+ m.CalledMethods["HumanType"]++
1065
+ if m.TypeFunc != nil {
1066
+ return m.TypeFunc(ref, text)
1067
+ }
1068
+ return nil
1069
+ }
1070
+
1071
+ func (m *MockPinchTabClient) GetText() (*pinchtab.TextResponse, error) {
1072
+ m.CalledMethods["GetText"]++
1073
+ if m.GetTextFunc != nil {
1074
+ return m.GetTextFunc()
1075
+ }
1076
+ return &pinchtab.TextResponse{}, nil
1077
+ }
1078
+ ```
1079
+
1080
+ **Step 2: Write navigator tests**
1081
+
1082
+ ```go
1083
+ package linkedin
1084
+
1085
+ import (
1086
+ "testing"
1087
+
1088
+ "github.com/thaddeus-git/linkedin-cli/internal/pinchtab"
1089
+ )
1090
+
1091
+ func TestFindConnectButton(t *testing.T) {
1092
+ mock := NewMockClient()
1093
+ navigator := NewNavigator(mock)
1094
+ navigator.sleeper = NoOpSleeper{}
1095
+
1096
+ snapshot := &pinchtab.Snapshot{
1097
+ Nodes: []pinchtab.Node{
1098
+ {Ref: "e0", Role: "button", Name: "View profile"},
1099
+ {Ref: "e1", Role: "button", Name: "Connect"},
1100
+ {Ref: "e2", Role: "link", Name: "Message"},
1101
+ },
1102
+ }
1103
+
1104
+ button, err := navigator.FindConnectButton(snapshot)
1105
+ if err != nil {
1106
+ t.Fatalf("unexpected error: %v", err)
1107
+ }
1108
+
1109
+ if button.Ref != "e1" {
1110
+ t.Errorf("expected ref e1, got %s", button.Ref)
1111
+ }
1112
+ }
1113
+
1114
+ func TestFindConnectButtonNotFound(t *testing.T) {
1115
+ mock := NewMockClient()
1116
+ navigator := NewNavigator(mock)
1117
+
1118
+ snapshot := &pinchtab.Snapshot{
1119
+ Nodes: []pinchtab.Node{
1120
+ {Ref: "e0", Role: "button", Name: "View profile"},
1121
+ {Ref: "e1", Role: "link", Name: "Message"},
1122
+ },
1123
+ }
1124
+
1125
+ _, err := navigator.FindConnectButton(snapshot)
1126
+ if err == nil {
1127
+ t.Error("expected error when connect button not found")
1128
+ }
1129
+ }
1130
+
1131
+ func TestClickConnect(t *testing.T) {
1132
+ mock := NewMockClient()
1133
+ mock.SnapshotFunc = func(filter string) (*pinchtab.Snapshot, error) {
1134
+ return &pinchtab.Snapshot{
1135
+ Nodes: []pinchtab.Node{
1136
+ {Ref: "e1", Role: "button", Name: "Connect"},
1137
+ },
1138
+ }, nil
1139
+ }
1140
+
1141
+ navigator := NewNavigator(mock)
1142
+ navigator.sleeper = NoOpSleeper{}
1143
+
1144
+ err := navigator.ClickConnect()
1145
+ if err != nil {
1146
+ t.Fatalf("unexpected error: %v", err)
1147
+ }
1148
+
1149
+ if mock.CalledMethods["GetSnapshot"] != 1 {
1150
+ t.Errorf("expected 1 GetSnapshot call, got %d", mock.CalledMethods["GetSnapshot"])
1151
+ }
1152
+
1153
+ if mock.CalledMethods["HumanClick"] != 1 {
1154
+ t.Errorf("expected 1 HumanClick call, got %d", mock.CalledMethods["HumanClick"])
1155
+ }
1156
+ }
1157
+ ```
1158
+
1159
+ **Step 3: Run tests**
1160
+
1161
+ ```bash
1162
+ go test -v ./internal/linkedin/...
1163
+ ```
1164
+
1165
+ Expected: All tests PASS
1166
+
1167
+ **Step 4: Commit**
1168
+
1169
+ ```bash
1170
+ git add internal/linkedin/navigator_test.go internal/linkedin/client_mock.go
1171
+ git commit -m "test: add navigator tests with mock client"
1172
+ ```
1173
+
1174
+ ---
1175
+
1176
+ ### Task 15: Update README with Badges
1177
+
1178
+ **Files:**
1179
+ - Modify: `README.md`
1180
+
1181
+ **Step 1: Add badges to top of README**
1182
+
1183
+ ```markdown
1184
+ # LinkedIn CLI
1185
+
1186
+ [![CI](https://github.com/thaddeus-git/linkedin-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/thaddeus-git/linkedin-cli/actions/workflows/ci.yml)
1187
+ [![Go Report Card](https://goreportcard.com/badge/github.com/thaddeus-git/linkedin-cli)](https://goreportcard.com/report/github.com/thaddeus-git/linkedin-cli)
1188
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1189
+ [![Go version](https://img.shields.io/badge/Go-1.21+-blue.svg)](https://go.dev)
1190
+
1191
+ A CLI tool for LinkedIn automation using PinchTab...
1192
+ ```
1193
+
1194
+ **Step 2: Add "How It Works" diagram**
1195
+
1196
+ ```markdown
1197
+ ## How It Works
1198
+
1199
+ ```
1200
+ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
1201
+ │ LinkedIn │◄────│ PinchTab │◄────│ linkedin-cli│
1202
+ │ (web) │ │ (browser) │ │ (CLI) │
1203
+ └─────────────┘ └──────────────┘ └─────────────┘
1204
+ ```
1205
+
1206
+ 1. **You run commands** → `linkedin connect --profile john --url ...`
1207
+ 2. **CLI validates input** → Check rate limits, validate URLs
1208
+ 3. **PinchTab controls browser** → Navigate, click, type
1209
+ 4. **LinkedIn receives actions** → As if from human user
1210
+ ```
1211
+
1212
+ **Step 3: Add troubleshooting section**
1213
+
1214
+ ```markdown
1215
+ ## Troubleshooting
1216
+
1217
+ ### "Profile not found"
1218
+
1219
+ Run `linkedin auth --profile <name>` first to authenticate.
1220
+
1221
+ ### "Rate limit exceeded"
1222
+
1223
+ Wait 24 hours for daily reset, or use a different profile.
1224
+
1225
+ ### "Connect button not found"
1226
+
1227
+ - Profile may already be connected
1228
+ - Profile URL may be incorrect
1229
+ - LinkedIn may have changed their UI (report as bug)
1230
+
1231
+ ### PinchTab not starting
1232
+
1233
+ ```bash
1234
+ curl -fsSL https://pinchtab.com/install.sh | bash
1235
+ pinchtab
1236
+ ```
1237
+
1238
+ ### Connection refused error
1239
+
1240
+ Make sure PinchTab is running:
1241
+
1242
+ ```bash
1243
+ curl http://localhost:9867/health
1244
+ ```
1245
+
1246
+ Should return: `{"status":"ok"}`
1247
+ ```
1248
+
1249
+ **Step 4: Commit**
1250
+
1251
+ ```bash
1252
+ git add README.md
1253
+ git commit -m "docs: update README with badges and troubleshooting"
1254
+ ```
1255
+
1256
+ ---
1257
+
1258
+ ### Task 16: Create Architecture Documentation
1259
+
1260
+ **Files:**
1261
+ - Create: `docs/ARCHITECTURE.md`
1262
+
1263
+ **Step 1: Write architecture documentation**
1264
+
1265
+ ```markdown
1266
+ # Architecture
1267
+
1268
+ This document describes the architecture of LinkedIn CLI.
1269
+
1270
+ ## Package Structure
1271
+
1272
+ ```
1273
+ linkedin-cli/
1274
+ ├── cmd/linkedin/ # Application entry point
1275
+ ├── internal/ # Private packages
1276
+ │ ├── cmd/ # Cobra command implementations
1277
+ │ ├── config/ # Configuration persistence
1278
+ │ ├── linkedin/ # LinkedIn page interactions
1279
+ │ ├── pinchtab/ # PinchTab HTTP client
1280
+ │ └── ratelimit/ # Rate limiting logic
1281
+ └── docs/ # Documentation
1282
+ ```
1283
+
1284
+ ## Package Responsibilities
1285
+
1286
+ ### cmd/linkedin
1287
+
1288
+ **Entry point:** `main()`
1289
+
1290
+ **Responsibilities:**
1291
+ - Parse command-line arguments
1292
+ - Execute appropriate command
1293
+ - Handle exit codes
1294
+
1295
+ ### internal/cmd
1296
+
1297
+ **Commands:**
1298
+ - `auth` - Authenticate LinkedIn profile
1299
+ - `connect` - Send connection request
1300
+ - `message` - Send direct message
1301
+ - `profiles` - List/remove profiles
1302
+
1303
+ **Responsibilities:**
1304
+ - Parse command flags
1305
+ - Validate inputs
1306
+ - Execute business logic
1307
+ - Display results to user
1308
+
1309
+ ### internal/config
1310
+
1311
+ **Types:**
1312
+ - `Manager` - Config file management
1313
+ - `Profile` - LinkedIn profile data
1314
+ - `RateLimits` - Rate limit state
1315
+
1316
+ **Responsibilities:**
1317
+ - Read/write JSON config files
1318
+ - Manage profile metadata
1319
+ - Persist rate limit counters
1320
+
1321
+ **Storage:**
1322
+ - `~/.linkedin-cli/profiles/<name>.json`
1323
+ - `~/.linkedin-cli/ratelimit.json`
1324
+
1325
+ ### internal/linkedin
1326
+
1327
+ **Types:**
1328
+ - `Navigator` - Page interaction logic
1329
+ - `Selectors` - CSS selectors for LinkedIn UI
1330
+
1331
+ **Responsibilities:**
1332
+ - Navigate to LinkedIn pages
1333
+ - Find and click buttons
1334
+ - Fill forms
1335
+ - Handle modals
1336
+
1337
+ **Note:** These selectors are fragile and may need updates as LinkedIn changes their UI.
1338
+
1339
+ ### internal/pinchtab
1340
+
1341
+ **Types:**
1342
+ - `Client` - HTTP API wrapper
1343
+
1344
+ **Responsibilities:**
1345
+ - Navigate browser
1346
+ - Get accessibility snapshots
1347
+ - Click/type elements
1348
+ - Extract page text
1349
+
1350
+ **API Endpoints Used:**
1351
+ - `POST /navigate`
1352
+ - `GET /snapshot`
1353
+ - `POST /action`
1354
+ - `GET /text`
1355
+
1356
+ ### internal/ratelimit
1357
+
1358
+ **Types:**
1359
+ - `Limiter` - Rate limit enforcement
1360
+ - `Limits` - Limit thresholds
1361
+
1362
+ **Responsibilities:**
1363
+ - Check if action is allowed
1364
+ - Record completed actions
1365
+ - Calculate random delays
1366
+ - Reset counters (daily/weekly)
1367
+
1368
+ ## Data Flow
1369
+
1370
+ ### Connection Request Flow
1371
+
1372
+ ```
1373
+ User → cmd/connect.go → Check rate limits
1374
+
1375
+ ratelimit/limiter.go → Load profile state
1376
+
1377
+ config.Manager → Read ~/.linkedin-cli/ratelimit.json
1378
+
1379
+ If allowed → linkedin.Navigator → PinchTab.Client
1380
+
1381
+ PinchTab API → Browser → LinkedIn
1382
+
1383
+ Success → Record in rate limits
1384
+ ```
1385
+
1386
+ ## Error Handling
1387
+
1388
+ **Pattern:** Wrap errors with context
1389
+
1390
+ ```go
1391
+ if err := navigator.ClickConnect(); err != nil {
1392
+ return fmt.Errorf("failed to click connect: %w", err)
1393
+ }
1394
+ ```
1395
+
1396
+ **User-facing errors:** Clear and actionable
1397
+
1398
+ ```go
1399
+ fmt.Fprintf(os.Stderr, "Error: Rate limit exceeded. Try again tomorrow.\n")
1400
+ ```
1401
+
1402
+ ## Testing Strategy
1403
+
1404
+ **Unit Tests:**
1405
+ - Config CRUD operations
1406
+ - URL validation
1407
+ - Rate limit logic
1408
+ - PinchTab client (mock HTTP)
1409
+
1410
+ **Integration Tests:**
1411
+ - Manual test script (`test_auth.sh`)
1412
+ - Future: automated with mock LinkedIn
1413
+
1414
+ **Test Files:**
1415
+ - `*_test.go` alongside source files
1416
+ - Mock implementations in `client_mock.go`
1417
+
1418
+ ## Dependencies
1419
+
1420
+ | Package | Purpose |
1421
+ |---------|---------|
1422
+ | `github.com/spf13/cobra` | CLI framework |
1423
+ | `log/slog` | Structured logging (Go 1.21+) |
1424
+ | PinchTab | Browser automation (external binary) |
1425
+
1426
+ ## Security Considerations
1427
+
1428
+ 1. **Credentials:** Never stored - user logs in manually
1429
+ 2. **Sessions:** Stored in PinchTab's Chrome profile (`~/.pinchtab/`)
1430
+ 3. **Config:** Local files only, no cloud sync
1431
+ 4. **Network:** Only localhost (PinchTab) and LinkedIn.com
1432
+
1433
+ ## Future Improvements
1434
+
1435
+ - [ ] LinkedIn selector auto-discovery
1436
+ - [ ] Proxy support
1437
+ - [ ] Multi-account rotation
1438
+ - [ ] Analytics dashboard
1439
+ - [ ] Webhook notifications
1440
+ ```
1441
+
1442
+ **Step 2: Commit**
1443
+
1444
+ ```bash
1445
+ git add docs/ARCHITECTURE.md
1446
+ git commit -m "docs: add architecture documentation"
1447
+ ```
1448
+
1449
+ ---
1450
+
1451
+ ### Task 17: Add CODEOWNERS and SECURITY.md
1452
+
1453
+ **Files:**
1454
+ - Create: `.github/CODEOWNERS`
1455
+ - Create: `docs/SECURITY.md`
1456
+
1457
+ **Step 1: Write CODEOWNERS**
1458
+
1459
+ ```
1460
+ # Default owners
1461
+ * @thaddeus-git
1462
+
1463
+ # Package-specific reviewers
1464
+ internal/linkedin/ @thaddeus-git
1465
+ internal/pinchtab/ @thaddeus-git
1466
+ ```
1467
+
1468
+ **Step 2: Write SECURITY.md**
1469
+
1470
+ ```markdown
1471
+ # Security Policy
1472
+
1473
+ ## Supported Versions
1474
+
1475
+ | Version | Supported |
1476
+ | ------- | ------------------ |
1477
+ | 0.x.x | :white_check_mark: |
1478
+
1479
+ ## Reporting a Vulnerability
1480
+
1481
+ **DO NOT create a public issue.**
1482
+
1483
+ Email: thaddeus@example.com (replace with actual)
1484
+
1485
+ Include:
1486
+ - Description of vulnerability
1487
+ - Steps to reproduce
1488
+ - Potential impact
1489
+ - Suggested fix (if any)
1490
+
1491
+ You will receive a response within 48 hours.
1492
+
1493
+ ## Security Considerations
1494
+
1495
+ ### What This Tool Does
1496
+
1497
+ - Automates LinkedIn browser interactions
1498
+ - Stores session cookies locally
1499
+ - Saves profile metadata
1500
+
1501
+ ### What This Tool Does NOT Do
1502
+
1503
+ - Upload data to cloud services
1504
+ - Store LinkedIn credentials
1505
+ - Modify LinkedIn's backend
1506
+
1507
+ ### Risks
1508
+
1509
+ 1. **LinkedIn Detection:** Automation may violate LinkedIn's ToS
1510
+ 2. **Account Restrictions:** LinkedIn may limit or ban accounts
1511
+ 3. **Local Data:** Session cookies stored on your machine
1512
+
1513
+ ### Mitigation
1514
+
1515
+ - Use rate limiting (enabled by default)
1516
+ - Use separate profiles for different accounts
1517
+ - Monitor account health
1518
+ - Don't exceed safe limits
1519
+
1520
+ ## Responsible Disclosure
1521
+
1522
+ We appreciate responsible disclosure and will work with you to address security issues promptly.
1523
+ ```
1524
+
1525
+ **Step 3: Commit**
1526
+
1527
+ ```bash
1528
+ git add .github/CODEOWNERS docs/SECURITY.md
1529
+ git commit -m "docs: add CODEOWNERS and security policy"
1530
+ ```
1531
+
1532
+ ---
1533
+
1534
+ ### Task 18: Final Verification and Cleanup
1535
+
1536
+ **Step 1: Run all tests**
1537
+
1538
+ ```bash
1539
+ go test -v -cover ./...
1540
+ ```
1541
+
1542
+ Expected: All tests PASS, coverage >= 70%
1543
+
1544
+ **Step 2: Run linter**
1545
+
1546
+ ```bash
1547
+ make lint
1548
+ ```
1549
+
1550
+ Expected: No errors (or fix them)
1551
+
1552
+ **Step 3: Format code**
1553
+
1554
+ ```bash
1555
+ go fmt ./...
1556
+ go vet ./...
1557
+ ```
1558
+
1559
+ **Step 4: Build for all platforms**
1560
+
1561
+ ```bash
1562
+ GOOS=linux GOARCH=amd64 go build -o linkedin-cli-linux ./cmd/linkedin
1563
+ GOOS=darwin GOARCH=arm64 go build -o linkedin-cli-mac ./cmd/linkedin
1564
+ GOOS=windows GOARCH=amd64 go build -o linkedin-cli.exe ./cmd/linkedin
1565
+ ```
1566
+
1567
+ **Step 5: Commit all changes**
1568
+
1569
+ ```bash
1570
+ git add .
1571
+ git commit -m "chore: final verification and cleanup for open source release"
1572
+ ```
1573
+
1574
+ ---
1575
+
1576
+ ## Verification Checklist
1577
+
1578
+ - [ ] LICENSE file exists
1579
+ - [ ] All tests pass
1580
+ - [ ] Coverage >= 70%
1581
+ - [ ] CONTRIBUTING.md exists
1582
+ - [ ] CI workflow runs
1583
+ - [ ] --version flag works
1584
+ - [ ] README has badges
1585
+ - [ ] Issue templates exist
1586
+ - [ ] PR template exists
1587
+ - [ ] Architecture docs complete
1588
+ - [ ] Security policy exists
1589
+
1590
+ ---
1591
+
1592
+ **Plan complete.** Open source publishing preparation is ready for implementation.