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.
- package/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- 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
|
+
[](https://github.com/thaddeus-git/linkedin-cli/actions/workflows/ci.yml)
|
|
1187
|
+
[](https://goreportcard.com/report/github.com/thaddeus-git/linkedin-cli)
|
|
1188
|
+
[](https://opensource.org/licenses/MIT)
|
|
1189
|
+
[](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.
|